CSS3盒子阴影+文本阴影技巧

CSS 阴影的存在,让物体看上去更加有型立体。

然而,在最简单的阴影使用之上,我们可以实现更多有意思且更加立体的阴影效果。

本文将带大家看看如何使用 CSS 实现几类比普通阴影更加立体的阴影效果。

CSS 阴影基础

CSS 中,明面上可以实现阴影的有三个属性:

  • box-shadow – 盒阴影
  • text-shadow – 文字阴影
  • filter: drop-shaodw() – 滤镜内的阴影

关于它们的基础语法和使用就不过多描述,这一部分大家可以先在 MDN 补齐,贴几张图快速复习一下:

box-shadow – 盒阴影:

text-shadow – 文字阴影:

filter: drop-shaodw() – 滤镜内的阴影:

基本上 3 种阴影大同小异,需要注意的就是:

  • box-shadow 还区分内阴影,内阴影使用关键字 inset 进行描述;
  • box-shadow 多一个阴影扩散半径参数。

长投影

上面提到了通过多层阴影叠加实现文字的立体阴影。运用在 div 这些容器上也是可以的。当然这里还有一种挺有意思的方法。假设我们,有一个矩形元素,希望给他添加一个长投影,像下面这样:

image

要生成这种长投影,刚刚说的叠加多层阴影可以,再就是借助元素的两个伪元素,其实上面的图是这样的:

image

关键点在于,我们通过对两个伪元素的 transform: skew() 变换以及从实色到透明色的背景色变化,实现了长投影的效果:

CodePen Demo — 线性渐变模拟长阴影

立体投影

好,我们继续。下一个主题是立体投影

这个说法很奇怪,阴影的出现,本就是为了让原本的元素看起来更加的立体,那这里所谓的立体投影,是个怎么立体法?

这里所谓的立体投影,并不一定是使用了 box-shadowtext-shadow 或者 drop-shadow,而是我们使用其他元素或者属性模拟元素的阴影。而这样做的目的,是为了能够突破 box-shadow 这类元素的一些定位局限。让阴影的位置、大小、模糊度可以更加的灵活

OK,让我们来看看,这样一个元素,我们希望通过自定义阴影的位置,让它更加立体:

image

上图 div 只是带了一个非常浅的 box-shadow ,看上去和立体没什么关系,接下来,我们通过 div 的伪元素,给它生成一个和原图边角形状类似的图形,再通过 transform 位移一下,可能是这样:

image

OK,最后对这个用伪元素生成的元素进行一些虚化效果(filter或者box-shadow都可以),就可以实现一个边角看起来像被撕开的立体效果:

image

代码非常简单,伪 CSS 代码示意如下:

div {
    position: relative;
    width: 600px;
    height: 100px;
    background: hsl(48, 100%, 50%);
    border-radius: 20px;
}

div::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 5%;
    right: 5%;
    bottom: 0;
    border-radius: 10px;
    background: hsl(48, 100%, 20%);
    transform: translate(0, -15%) rotate(-4deg);
    transform-origin: center center;
    box-shadow: 0 0 20px 15px hsl(48, 100%, 20%);
}

所以总结一下:

  • 立体投影的关键点在于利于伪元素生成一个大小与父元素相近的元素,然后对其进行 rotate 以及定位到合适位置,再赋于阴影操作
  • 颜色的运用也很重要,阴影的颜色通常比本身颜色要更深,这里使用 hsl 表示颜色更容易操作,l 控制颜色的明暗度

还有其他很多场景,都可以用类似的技巧实现:

image

详细完整的代码,你可以戳这里:CodePen Demo — 立体投影

浮雕阴影

还有一类立体效果的阴影就是浮雕阴影,它的本质还是 box-shadow 和 text-shadow,只是需要控制好颜色的配合以及内外阴影的一起使用。核心就是 2 点:

  1. 背景色与内容(文本或者盒子颜色)一致
  2. 使用两个相反的方向,使用两组对比明显的颜色值,来实现凹凸效果

首先,我们来看一个文字版的浮雕效果。

先实现一个凸起的效果,我们需要实现一个背景色和文字色一样的文字:

<div>浮雕阴影</div>
body {
    background: #999;
}
p {
    color: #999;
}

效果如下,由于背景色和文字色的颜色一样,所以我们什么都看不到。

不过没事,我们给文字添加一个 1px x、y 方向的黑色阴影:

p {
    color: #999;
    text-shadow: 1px 1px 1px #000;
}

效果如下:

有点感觉了,再反向,也就是 -1px x、y 方向添加一个黑色相对,也就白色的阴影:

p {
    color: #999;
    text-shadow: 
        1px 1px 1px #000, 
        -1px -1px 1px #fff;
}

效果如下,这样我们就得到了一个凸起质感的浮雕阴影:

如果我们把颜色对调一下呢?

p {
    color: #999;
    text-shadow: 
        1px 1px 1px #fff, 
        -1px -1px 1px #000;
}

就能很轻松的得到凹下质感的浮雕阴影:

上述 DEMO 的完整代码:CodePen Demo – Embossed Shadow

新拟态风格阴影

我们将运用在文字上的技巧,扩展延伸到容器上,就能得到最近比较火的拟态风格阴影,其原理也是大同小异。

两个阴影,使用两个相反的方向,使用两组对比明显的颜色值,来实现凹凸效果。与文字不同的是,这里的凹效果,我们需要使用盒子的内阴影实现

<div>浮雕阴影</div>
<div>浮雕阴影</div>
div {
    width: 120px;
    height: 120px;
    background: #e9ecef;
    color: #333;
    box-shadow:
        7px 7px 12px rgba(0, 0, 0, .4),
        -7px -7px 12px rgba(255, 255, 255, .9);
}

div:nth-child(2) {
    box-shadow:
        inset -7px -7px 12px rgba(255, 255, 255, .9),
        inset 7px 7px 12px rgba(0, 0, 0, .4);
}

这样,就可以得到拟态风格的按钮,如下图所示,左凸右凹:

再通过一个简单的过渡,就可以实现整个点击的交互:

div {
    transition: .2s all;
    box-shadow:
        7px 7px 12px rgba(0, 0, 0, .4),
        -7px -7px 12px rgba(255, 255, 255, .9),
        inset 0 0 0x rgba(255, 255, 255, .9),
        inset 0 0 0 rgba(0, 0, 0, .4);
    
    &:active {
        box-shadow:
            0 0 0 rgba(0, 0, 0, .4),
            0 0 0 rgba(255, 255, 255, .9),
            inset -7px -7px 12px rgba(255, 255, 255, .9),
            inset 7px 7px 12px rgba(0, 0, 0, .4);
    }
}

看看效果:

文字立体投影 / 文字长阴影

上面的立体效果在文字上就完全不适用了,所以对待文字的立体阴影效果,还需要另辟蹊径。

正常而言,我们使用 text-shadow 来生成文字阴影,像这样:

<div> Txt Shadow</div>
-----
div {
    text-shadow: 6px 6px 3px hsla(14, 100%, 30%, 1);
}

image

嗯,挺好的,就是不够立体。那么要做到立体文字阴影,最常见的方法就是使用多层文字阴影叠加。

Tips:和 box-shadow 一样,text-shadow 是可以叠加多层的!但是对于单个元素而言, drop-shadow 的话就只能是一层。

好,上面的文字,我们试着叠加个 50 层文字阴影试一下。额,50 层手写,其实很快的~

好吧,手写真的太慢了,还容易出错,所以这里我们需要借助一下 SASS/LESS 帮忙,写一个生成 50 层阴影的 function 就好,我们每向右和向下偏移 1px,生成一层 text-shadow:

@function makeLongShadow($color) {
    $val: 0px 0px $color;

    @for $i from 1 through 50 {
        $val: #{$val}, #{$i}px #{$i}px #{$color};
    }

    @return $val;
}

div {
    text-shadow: makeLongShadow(hsl(14, 100%, 30%));
}

上面的 SCSS 代码。经过编译后,就会生成如下 CSS:

div {
      text-shadow: 0px 0px #992400, 1px 1px #992400, 2px 2px #992400, 3px 3px #992400, 4px 4px #992400, 5px 5px #992400, 6px 6px #992400, 7px 7px #992400, 8px 8px #992400, 9px 9px #992400, 10px 10px #992400, 11px 11px #992400, 12px 12px #992400, 13px 13px #992400, 14px 14px #992400, 15px 15px #992400, 16px 16px #992400, 17px 17px #992400, 18px 18px #992400, 19px 19px #992400, 20px 20px #992400, 21px 21px #992400, 22px 22px #992400, 23px 23px #992400, 24px 24px #992400, 25px 25px #992400, 26px 26px #992400, 27px 27px #992400, 28px 28px #992400, 29px 29px #992400, 30px 30px #992400, 31px 31px #992400, 32px 32px #992400, 33px 33px #992400, 34px 34px #992400, 35px 35px #992400, 36px 36px #992400, 37px 37px #992400, 38px 38px #992400, 39px 39px #992400, 40px 40px #992400, 41px 41px #992400, 42px 42px #992400, 43px 43px #992400, 44px 44px #992400, 45px 45px #992400, 46px 46px #992400, 47px 47px #992400, 48px 48px #992400, 49px 49px #992400, 50px 50px #992400;
}

看看效果:

image

额,很不错,很立体。但是,就是丑,而且说不上来的奇怪。

问题出在哪里呢,阴影其实是存在明暗度和透明度的变化的,所以,对于渐进的每一层文字阴影,明暗度和透明度应该都是不断变化的。这个需求,SASS 可以很好的实现,下面是两个 SASS 颜色函数:

  • fade-out 改变颜色的透明度,让颜色更加透明
  • desaturate 改变颜色的饱和度值,让颜色更少的饱和

关于 SASS 颜色函数,可以看看这里:Sass基础—颜色函数

我们使用上面两个 SASS 颜色函数修改一下我们的 CSS 代码,主要是修改上面的 makeLongShadow function 函数:

@function makelongrightshadow($color) {
    $val: 0px 0px $color;

    @for $i from 1 through 50 {
        $color: fade-out(desaturate($color, 1%), .02);
        $val: #{$val}, #{$i}px #{$i}px #{$color};
    }

    @return $val;
}

好,看看最终效果:

嗯,大功告成,这次顺眼了很多~

详细完整的代码,你可以戳这里:CodePen Demo — 立体文字阴影

当然,使用 CSS 生成立体文字阴影的方法还有很多,下面再贴出一例,使用了透明色叠加底色的多重线性渐变实现的文字立体阴影,感兴趣的同学可以去看看具体实现:

详细完整的代码,你可以戳这里:CodePen Demo — 线性渐变配合阴影实现条纹立体阴影条纹字

来源:https://github.com/chokcoco/iCSS/issues/170

Coco chokcoco 国服第一切图仔

CSS Animations 入门与进阶

CSS 动画Animations 入门与进阶

本文将比较全面细致的梳理一下 CSS 动画的方方面面,针对每个属性用法的讲解及进阶用法的示意,希望能成为一个比较好的从入门到进阶的教程。

CSS 动画介绍及语法

首先,我们来简单介绍一下 CSS 动画。

最新版本的 CSS 动画由规范 — CSS Animations Level 1定义。

CSS 动画用于实现元素从一个 CSS 样式配置转换到另一个 CSS 样式配置。

动画包括两个部分: 描述动画的样式规则和用于指定动画开始、结束以及中间点样式的关键帧。

简单来说,看下面的例子:

 div {
     animation: change 3s;
 }
 ​
 @keyframes change {
     0% {
         color: #f00;
     }
     100% {
         color: #000;
     }
 }
  • animation: move 1s 部分就是动画的第一部分,用于描述动画的各个规则;

  • @keyframes move {} 部分就是动画的第二部分,用于指定动画开始、结束以及中间点样式的关键帧;

一个 CSS 动画一定要由上述两部分组成。

CSS 动画的语法

接下来,我们简单看看 CSS 动画的语法。

创建动画序列,需要使用 animation 属性或其子属性,该属性允许配置动画时间、时长以及其他动画细节,但该属性不能配置动画的实际表现,动画的实际表现是由 @keyframes 规则实现。

animation 的子属性有:

  • animation-name:指定由 @keyframes 描述的关键帧名称。

  • animation-duration:设置动画一个周期的时长。

  • animation-delay:设置延时,即从元素加载完成之后到动画序列开始执行的这段时间。

  • animation-direction:设置动画在每次运行完后是反向运行还是重新回到开始位置重复运行。

  • animation-iteration-count:设置动画重复次数, 可以指定 infinite 无限次重复动画

  • animation-play-state:允许暂停和恢复动画。

  • animation-timing-function:设置动画速度, 即通过建立加速度曲线,设置动画在关键帧之间是如何变化。

  • animation-fill-mode:指定动画执行前后如何为目标元素应用样式

  • @keyframes 规则,当然,一个动画想要运行,还应该包括 @keyframes 规则,在内部设定动画关键帧

其中,对于一个动画:

  • 必须项:animation-name、animation-duration 和 @keyframes规则

  • 非必须项:animation-delay、animation-direction、animation-iteration-count、animation-play-state、animation-timing-function、animation-fill-mode,当然不是说它们不重要,只是不设置时,它们都有默认值

上面已经给了一个简单的 DEMO, 就用上述的 DEMO,看看结果:

这就是一个最基本的 CSS 动画,本文将从 animation 的各个子属性入手,探究 CSS 动画的方方面面。

animation-name / animation-duration 详解

整体而言,单个的 animation-name 和 animation-duration 没有太多的技巧,非常好理解,放在一起。

首先介绍一下 animation-name,通过 animation-name,CSS 引擎将会找到对应的 @keyframes 规则。

08-anmistion-009

当然,它和 CSS 规则命名一样,也存在一些骚操作。譬如,他是支持 emoji 表情的,所以代码中的 animation-name 命名也可以这样写:

div {
 animation: face 3s;
}

@keyframes face {
 0% {
 color: #f00;
 }
 100% {
 color: #000;
 }
}

而 animation-duration 设置动画一个周期的时长,上述 DEMO 中,就是设定动画整体持续 3s,这个也非常好理解。

animation-delay 详解

animation-delay 就比较有意思了,它可以设置动画延时,即从元素加载完成之后到动画序列开始执行的这段时间。

简单的一个 DEMO:

 <div></div>
 <div></div>
 div {
     width: 100px;
     height: 100px;
     background: #000;
     animation-name: move;
     animation-duration: 2s;
 }
 ​
 div:nth-child(2) {
     animation-delay: 1s;
 }
 @keyframes move {
     0% {
         transform: translate(0);
     }
     100% {
         transform: translate(200px);
     }
 }

比较下列两个动画,一个添加了 animation-delay,一个没有,非常直观:

08-anmistion-012 08-anmistion-013 08-anmistion-014

上述第二个 div,关于 animation 属性,也可以简写为 animation: move 2s 1s,第一个时间值表示持续时间,第二个时间值表示延迟时间。

animation-delay 可以为负值

关于 animation-delay,最有意思的技巧在于,它可以是负数。也就是说,虽然属性名是动画延迟时间,但是运用了负数之后,动画可以提前进行。

假设我们要实现这样一个 loading 动画效果:

08-anmistion-006

有几种思路:

  • 初始 3 个球的位置就是间隔 120°,同时开始旋转,但是这样代码量会稍微多一点

  • 另外一种思路,同一个动画,3 个元素的其中两个延迟整个动画的 1/3,2/3 时间出发

方案 2 的核心伪代码如下:

 .item:nth-child(1) {
     animation: rotate 3s infinite linear;
 }
 .item:nth-child(2) {
     animation: rotate 3s infinite 1s linear;
 }
 .item:nth-child(3) {
     animation: rotate 3s infinite 2s linear;
 }

但是,在动画的前 2s,另外两个元素是不会动的,只有 2s 过后,整个动画才是我们想要的:

08-anmistion-005

此时,我们可以让第 2、3 个元素的延迟时间,改为负值,这样可以让动画延迟进行 -1s、-2s,也就是提前进行 1s、2s:

 .item:nth-child(1) {
     animation: rotate 3s infinite linear;
 }
 .item:nth-child(2) {
     animation: rotate 3s infinite -1s linear;
 }
 .item:nth-child(3) {
     animation: rotate 3s infinite -2s linear;
 }

这样,每个元素都无需等待,直接就是运动状态中的,并且元素间隔位置是我们想要的结果:

利用 animation-duration 和 animation-delay 构建随机效果

还有一个有意思的小技巧。

同一个动画,我们利用一定范围内随机的 animation-duration 和一定范围内随机的 animation-delay,可以有效的构建更为随机的动画效果,让动画更加的自然。

我在下述两个纯 CSS 动画中,都使用了这样的技巧:

纯 CSS 实现华为充电动画:

08-anmistion-009纯 CSS 实现华为充电动画

【第1816期】巧用 CSS 实现酷炫的充电动画

纯 CSS 实现火焰动画

08-anmistion-008纯 CSS 实现火焰动画

【第1568期】CSS 火焰

以纯 CSS 实现华为充电动画为例子,简单讲解一下。

仔细观察这一部分,上升的一个一个圆球,抛去这里的一些融合效果,只关注不断上升的圆球,看着像是没有什么规律可言:

08-anmistion-011

我们来模拟一下,如果是使用 10 个 animation-duration 和 animation-delay 都一致的圆的话,核心伪代码:

 <ul>
     <li></li>
     <!--共 10 个...--> 
     <li></li>
 </ul>
 ul {
     display: flex;
     flex-wrap: nowrap;
     gap: 5px;
 }
 li {
     background: #000;
     animation: move 3s infinite 1s linear;
 }
 @keyframes move {
     0% {
         transform: translate(0, 0);
     }
     100% {
         transform: translate(0, -100px);
     }
 }

这样,小球的运动会是这样的整齐划一:

08-anmistion-010

要让小球的运动显得非常的随机,只需要让 animation-duration 和 animation-delay 都在一定范围内浮动即可,改造下 CSS:

 @for $i from 1 to 11 {
     li:nth-child(#{$i}) {
         animation-duration: #{random(2000)/1000 + 2}s;
         animation-delay: #{random(1000)/1000 + 1}s;
     }
 }

我们利用 SASS 的循环和 random() 函数,让 animation-duration 在 2-4 秒范围内随机,让 animation-delay 在 1-2 秒范围内随机,这样,我们就可以得到非常自然且不同的上升动画效果,基本不会出现重复的画面,很好的模拟了随机效果:

08-anmistion-011

CodePen Demo — 利用范围随机 animation-duration 和 animation-delay 实现随机动画效果

animation-timing-function 缓动函数

缓动函数在动画中非常重要,它定义了动画在每一动画周期中执行的节奏。

缓动主要分为两类:

  • cubic-bezier-timing-function 三次贝塞尔曲线缓动函数

  • step-timing-function 步骤缓动函数(这个翻译是我自己翻的,可能有点奇怪)

三次贝塞尔曲线缓动函数

首先先看看三次贝塞尔曲线缓动函数。在 CSS 中,支持一些缓动函数关键字。

 /* Keyword values */
 animation-timing-function: ease;  // 动画以低速开始,然后加快,在结束前变慢
 animation-timing-function: ease-in;  // 动画以低速开始
 animation-timing-function: ease-out; // 动画以低速结束
 animation-timing-function: ease-in-out; // 动画以低速开始和结束
 animation-timing-function: linear; // 匀速,动画从头到尾的速度是相同的

08-anmistion-015

除了 CSS 支持的这 5 个关键字,我们还可以使用 cubic-bezier() 方法自定义三次贝塞尔曲线:

 animation-timing-function: cubic-bezier(0.1, 0.7, 1.0, 0.1);

这里有个非常好用的网站 — cubic-bezier用于创建和调试生成不同的贝塞尔曲线参数。

三次贝塞尔曲线缓动对动画的影响

关于缓动函数对动画的影响,这里有一个非常好的示例。这里我们使用了纯 CSS 实现了一个钟的效果,对于其中的动画的运动,如果是 animation-timing-function: linear,效果如下:

08-anmistion-016

而如果我们我把缓动函数替换一下,变成 animation-timing-function: cubic-bezier(1,-0.21,.85,1.29),它的曲线对应如下:

08-anmistion-014

整个钟的动画律动效果将变成这样,完全不一样的感觉:

CodePen Demo – 缓动不同效果不同

对于许多精益求精的动画,在设计中其实都考虑到了缓动函数。很久之前看到过一篇《基于物理学的动画用户体验设计》,可惜如今已经无法找到原文。其中传达出的一些概念是,动画的设计依据实际在生活中的表现去考量。

譬如 linear 这个缓动,实际应用于某些动画中会显得很不自然,因为由于空气阻力的存在,程序模拟的匀速直线运动在现实生活中是很难实现的。因此对于这样一个用户平时很少感知到的运动是很难建立信任感的。这样的匀速直线运动也是我们在进行动效设计时需要极力避免的。

步骤缓动函数

接下来再讲讲步骤缓动函数。在 CSS 的 animation-timing-function 中,它有如下几种表现形态:

 {
     /* Keyword values */
     animation-timing-function: step-start;
     animation-timing-function: step-end;
 ​
     /* Function values */
     animation-timing-function: steps(6, start)
     animation-timing-function: steps(4, end);
 }

在 CSS 中,使用步骤缓动函数最多的,就是利用其来实现逐帧动画。假设我们有这样一张图(图片大小为 1536 x 256,图片来源于网络):

08-anmistion-018

可以发现它其实是一个人物行进过程中的 6 种状态,或者可以为 6 帧,我们利用 animation-timing-function: steps(6) 可以将其用一个 CSS 动画串联起来,代码非常的简单:

 <div class="box"></div>
 .box {
   width: 256px;
   height: 256px;
   background: url('https://github.com/iamalperen/playground/blob/main/SpriteSheetAnimation/sprite.png?raw=true');
   animation: sprite .6s steps(6, end) infinite;
 }
 @keyframes sprite {
   0% { 
     background-position: 0 0;
   }
   100% { 
     background-position: -1536px 0;
   }
 }

简单解释一下上述代码,首先要知道,刚好 256 x 6 = 1536,所以上述图片其实可以刚好均分为 6 段:

  • 我们设定了一个大小都为 256px 的 div,给这个 div 赋予了一个 animation: sprite .6s steps(6) infinite 动画;

  • 其中 steps(6) 的意思就是将设定的 @keyframes 动画分为 6 次(6帧)执行,而整体的动画时间是 0.6s,所以每一帧的停顿时长为 0.1s;

  • 动画效果是由 background-position: 0 0 到 background-position: -1536px 0,由于上述的 CSS 代码没有设置 background-repeat,所以其实 background-position: 0 0 是等价于 background-position: -1536px 0,就是图片在整个动画过程中推进了一轮,只不过每一帧停在了特点的地方,一共 6 帧;

将上述 1、2、3,3 个步骤画在图上简单示意:

08-anmistion-019

从上图可知,其实在动画过程中,background-position 的取值其实只有 background-position: 0 0,background-position: -256px 0,background-position: -512px 0 依次类推一直到 background-position: -1536px 0,由于背景的 repeat 的特性,其实刚好回到原点,由此又重新开始新一轮同样的动画。

所以,整个动画就会是这样,每一帧停留 0.1s 后切换到下一帧(注意这里是个无限循环动画),:

08-anmistion-019

完整的代码你可以戳这里 — CodePen Demo — Sprite Animation with steps()

animation-duration 动画长短对动画的影响

在这里再插入一个小章节,animation-duration 动画长短对动画的影响也是非常明显的。

在上述代码的基础上,我们再修改 animation-duration,缩短每一帧的时间就可以让步行的效果变成跑步的效果,同理,也可以增加每一帧的停留时间。让每一步变得缓慢,就像是在步行一样。

需要提出的是,上文说的每一帧,和浏览器渲染过程中的 FPS 的每一帧不是同一个概念。

看看效果,设置不同的 animation-duration 的效果(这里是 0.6s -> 0.2s),GIF 录屏丢失了一些关键帧,实际效果会更好点:

08-anmistion-020

当然,在 steps() 中,还有 steps(6, start) 和 steps(6, end) 的差异,也就是其中关键字 start 和 end 的差异。对于上述的无限动画而言,其实基本是可以忽略不计的,它主要是控制动画第一帧的开始和持续时长,比较小的一个知识点但是想讲明白需要比较长的篇幅,限于本文的内容,在这里不做展开,读者可以自行了解。

同个动画效果的补间动画和逐帧动画演绎对比

上述的三次贝塞尔曲线缓动和步骤缓动,其实就是对应的补间动画和逐帧动画。

对于同个动画而言,有的时候两种缓动都是适用的。我们在具体使用的时候需要具体分析选取。

假设我们用 CSS 实现了这样一个图形:

08-anmistion-021

现在想利用这个图形制作一个 Loading 效果,如果利用补间动画,也就是三次贝塞尔曲线缓动的话,让它旋转起来,得到的效果非常的一般:

 .g-container{
     animation: rotate 2s linear infinite;
 }
 @keyframes rotate {
     0% {
         transform: rotate(0);
     }
     100% {
         transform: rotate(360deg);
     }
 }

动画效果如下:

08-anmistion-022

但是如果这里,我们将补间动画换成逐帧动画,因为有 20 个点,所以设置成 steps(20),再看看效果,会得到完全不一样的感觉:

 .g-container{
     animation: rotate 2s steps(20) infinite;
 }
 @keyframes rotate {
     0% {
         transform: rotate(0);
     }
     100% {
         transform: rotate(360deg);
     }
 }

动画效果如下:

08-anmistion-023

整个 loading 的圈圈看上去好像也在旋转,实际上只是 20 帧关键帧在切换,整体的效果感觉更适合 Loading 的效果。

因此,两种动画效果都是很有必要掌握的,在实际使用的时候灵活尝试,选择更适合的。

上述 DEMO 效果完整的代码:CodePen Demo — Scale Loading steps vs linear

animation-play-state

接下来,我们讲讲 animation-play-state,顾名思义,它可以控制动画的状态 — 运行或者暂停。类似于视频播放器的开始和暂停。是 CSS 动画中有限的控制动画状态的手段之一。

它的取值只有两个(默认为 running):

 {
     animation-play-state: paused | running;
 }

使用起来也非常简单,看下面这个例子,我们在 hover 按钮的时候,实现动画的暂停:

 <div class="btn stop">stop</div>
 <div class="animation"></div>
 .animation {
     width: 100px;
     height: 100px;
     background: deeppink;
     animation: move 2s linear infinite alternate;
 }
 ​
 @keyframes move {
     100% {
         transform: translate(100px, 0);
     }
 }
 ​
 .stop:hover ~ .animation {
     animation-play-state: paused;
 }

一个简单的 CSS 动画,但是当我们 hover 按钮的时候,给动画元素添加上 animation-play-state: paused:

08-anmistion-024

animation-play-state 小技巧,默认暂停,点击运行

正常而言,按照正常思路使用 animation-play-state: paused 是非常简单的。

但是,如果我们想创造一些有意思的 CSS 动画效果,不如反其道而行之。

我们都知道,正常情况下,动画应该是运行状态,那如果我们将一些动画的默认状态设置为暂停,只有当鼠标点击或者 hover 的时候,才设置其 animation-play-state: running,这样就可以得到很多有趣的 CSS 效果。

看个倒酒的例子,这是一个纯 CSS 动画,但是默认状态下,动画处于 animation-play-state: paused,也就是暂停状态,只有当鼠标点击杯子的时,才设置 animation-play-state: running,让酒倒下,

08-anmistion-025

完整的 DEMO 你可以戳这里:CodePen Demo — CSS Beer!

在非常多 Web 创意交互动画我们都可以看到这个技巧的身影。

页面 render 后,无任何操作,动画不会开始。只有当鼠标对元素进行 click ,通过触发元素的 :active 伪类效果的时候,赋予动画 animation-play-state: running,动画才开始进行;动画进行到任意时刻,鼠标停止点击,伪类消失,则动画停止;

animation-fill-mode 控制元素在各个阶段的状态

下一个属性 animation-fill-mode,很多人会误认为它只是用于控制元素在动画结束后是否复位。这个其实是不准确的,不全面的。

看看它的取值:

 {
     // 默认值,当动画未执行时,动画将不会将任何样式应用于目标,而是使用赋予给该元素的 CSS 规则来显示该元素的状态
     animation-fill-mode: none;
     // 动画将在应用于目标时立即应用第一个关键帧中定义的值,并在 `animation-delay` 期间保留此值,
     animation-fill-mode: backwards; 
     // 目标将保留由执行期间遇到的最后一个关键帧计算值。最后一个关键帧取决于 `animation-direction` 和 `animation-iteration-count`
     animation-fill-mode: forwards;    
     // 动画将遵循 `forwards` 和 `backwards` 的规则,从而在两个方向上扩展动画属性
     animation-fill-mode: both; 
 }

对于 animation-fill-mode 的解读,我在 Segment Fault 上的一个问答中(SF – 如何理解 animation-fill-mode)看到了 4 副很好的解读图,这里借用一下:

假设 HTML 如下:

 <div class="box"></div>

CSS如下:

 .box{
     transform: translateY(0);
 }
 .box.on{
     animation: move 1s;
 }
 ​
 @keyframes move{
     from{transform: translateY(-50px)}
     to  {transform: translateY( 50px)}
 }

使用图片来表示 translateY 的值与 时间 的关系:

  • 横轴为表示 时间,为 0 时表示动画开始的时间,也就是向 box 加上 on 类名的时间,横轴一格表示 0.5s

  • 纵轴表示 translateY 的值,为 0 时表示 translateY 的值为 0,纵轴一格表示 50px

1、animation-fill-mode: none 表现如图:

一句话总结,元素在动画时间之外,样式只受到它的 CSS 规则限制,与 @keyframes 内的关键帧定义无关。

2、animation-fill-mode: backwards 表现如图:

08-anmistion-025

一句话总结,元素在动画开始之前(包含未触发动画阶段及 animation-delay 期间)的样式为动画运行时的第一帧,而动画结束后的样式则恢复为 CSS 规则设定的样式。

3、animation-fill-mode: forwards 

一句话总结,元素在动画开始之前的样式为 CSS 规则设定的样式,而动画结束后的样式则表现为由执行期间遇到的最后一个关键帧计算值(也就是停在最后一帧)。

4、animation-fill-mode: both 

一话总结,综合了 animation-fill-mode: backwards 和 animation-fill-mode: forwards 的设定。动画开始前的样式为动画运行时的第一帧,动画结束后停在最后一帧。

animation-iteration-count/animation-direction 动画循环次数和方向

讲到了 animation-fill-mode,我们就可以顺带讲讲这个两个比较好理解的属性 — animation-iteration-count 和 animation-direction

  • animation-iteration-count 控制动画运行的次数,可以是数字或者 infinite,注意,数字可以是小数

  • animation-direction 控制动画的方向,正向、反向、正向交替与反向交替

在上面讲述 animation-fill-mode 时,我使用了动画运行时的第一帧替代了@keyframes 中定义的第一帧这种说法,因为动画运行的第一帧和最后一帧的实际状态还会受到动画运行方向 animation-direction 和 animation-iteration-count 的影响。

在 CSS 动画中,由 animation-iteration-count 和 animation-direction 共同决定动画运行时的第一帧和最后一帧的状态。

  • 动画运行的第一帧由 animation-direction 决定

  • 动画运行的最后一帧由 animation-iteration-count 和 animation-direction 决定

动画的最后一帧,也就是动画运行的最终状态,并且我们可以利用 animation-fill-mode: forwards 让动画在结束后停留在这一帧,这个还是比较好理解的,但是 animation-fill-mode: backwards 和 animation-direction 的关系很容易弄不清楚,这里简答讲解下。

设置一个 100px x 100px 的滑块,在一个 400px x 100px 的容器中,其代码如下:

 <div class="g-father">
     <div class="g-box"></div>
 </div>
 .g-father {
     width: 400px;
     height: 100px;
     border: 1px solid #000;
 }
 .g-box {
     width: 100px;
     height: 100px;
     background: #333;
 }

表现如下:

那么,加入 animation 之后,在不同的 animation-iteration-count 和 animation-direction 作用下,动画的初始和结束状态都不一样。

如果设置了 animation-fill-mode: backwards,则元素在动画未开始前的状态由 animation-direction 决定:

 .g-box {
     ...
     animation: move 4s linear;
     animation-play-state: paused;
     transform: translate(0, 0);
 }
 @keyframes move {
     0% {
         transform: translate(100px, 0);
     }
     100% {
         transform: translate(300px, 0);
     }
 }

注意这里 CSS 规则中,元素没有设置位移 transform: translate(0, 0),而在动画中,第一个关键帧和最后一个关键的 translateX 分别是 100px、300px,配合不同的 animation-direction 初始状态如下。

下图假设我们设置了动画默认是暂停的 — animation-play-state: paused,那么动画在开始前的状态为:

动画的分治与复用

讲完了每一个属性,我们再来看看一些动画使用过程中的细节。

看这样一个动画:

 <div></div>
 div {
     width: 100px;
     height: 100px;
     background: #000;
     animation: combine 2s;
 }
 @keyframes combine {
     100% {
         transform: translate(0, 150px);
         opacity: 0;
     }
 }

这里我们实现了一个 div 块下落动画,下落的同时产生透明度的变化:

对于这样一个多个属性变化的动画,它其实等价于:

 div {
     animation: falldown 2s, fadeIn 2s;
 }
 ​
 @keyframes falldown {
     100% {
         transform: translate(0, 150px);
     }
 }
 @keyframes fadeIn {
     100% {
         opacity: 0;
     }
 }

在 CSS 动画规则中,animation 是可以接收多个动画的,这样做的目的不仅仅只是为了复用,同时也是为了分治,我们对每一个属性层面的动画能够有着更为精确的控制。

keyframes 规则的设定

我们经常能够在各种不同的 CSS 代码见到如下两种 CSS @keyframes 的设定:

1、使用百分比

 @keyframes fadeIn {
     0% {
         opacity: 1;
     }
     100% {
         opacity: 0;
     }
 }

2、使用 from 及 to

 @keyframes fadeIn {
     from {
         opacity: 1;
     }
     to {
         opacity: 0;
     }
 }

在 CSS 动画 @keyframes 的定义中,from 等同于 0%,而 to 等同于 100%。

当然,当我们的关键帧不止 2 帧的时,更推荐使用百分比定义的方式。

除此之外,当动画的起始帧等同于 CSS 规则中赋予的值并且没有设定 animation-fill-mode,0% 和 from 这一帧是可以删除的。

动画状态的高优先级性

我曾经在这篇文章中 — 深入理解 CSS(Cascading Style Sheets)中的层叠(Cascading)讲过一个很有意思的 CSS 现象。

这也是很多人对 CSS 优先级的一个认知误区,在 CSS 中,优先级还需要考虑选择器的层叠(级联)顺序。

只有在层叠顺序相等时,使用哪个值才取决于样式的优先级。

那什么是层叠顺序呢?

根据 CSS Cascading 4 最新标准:CSS Cascading and Inheritance Level 5(Current Work)

定义的当前规范下申明的层叠顺序优先级如下(越往下的优先级越高,下面的规则按升序排列):

  • Normal user agent declarations

  • Normal user declarations

  • Normal author declarations

  • Animation declarations

  • Important author declarations

  • Important user declarations

  • Important user agent declarations

  • Transition declarations

简单翻译一下:

按照上述算法,大概是这样:

过渡动画过程中每一帧的样式 > 用户代理、用户、页面作者设置的!important样式 > 动画过程中每一帧的样式优先级 > 页面作者、用户、用户代理普通样式。

然而,经过多个浏览器的测试,实际上并不是这样。(尴尬了)

举个例子,我们可以通过这个特性,覆盖掉行内样式中的 !important 样式:

 <p class="txt" style="color: red!important;">123456789</p>
 .txt {
     animation: colorGreen 2s infinite;
 }
 @keyframes colorGreen {
     0%,
     100% {
         color: green;
     }
 }

在 Safari 浏览器下,上述 DEMO 文本的颜色为绿色,也就是说,处于动画状态中的样式,能够覆盖掉行内样式中的 !important 样式,属于最最高优先级的一种样式,我们可以通过无限动画、或者 animation-fill-mode: forwards,利用这个技巧,覆盖掉本来应该是优先级非常非常高的行内样式中的 !important 样式。

我在早两年的 Chrome 中也能得到同样的结果,但是到今天(2022-01-10),最新版的 Chrome 已经不支持动画过程中关键帧样式优先级覆盖行内样式 !important 的特性。

对于不同浏览器,感兴趣的同学可以利用我这个 DEMO 自行尝试,CodePen Demo – the priority of CSS Animation

CSS 动画的优化

这也是非常多人非常关心的一个重点。

我的 CSS 动画很卡,我应该如何去优化它?

动画元素生成独立的 GraphicsLayer,强制开始 GPU 加速

CSS 动画很卡,其实是一个现象描述,它的本质其实是在动画过程中,浏览器刷新渲染页面的帧率过低。通常而言,目前大多数浏览器刷新率为 60 次/秒,所以通常来讲 FPS 为 60 frame/s 时动画效果较好,也就是每帧的消耗时间为 16.67ms。

页面处于动画变化时,当帧率低于一定数值时,我们就感觉到页面的卡顿。

而造成帧率低的原因就是浏览器在一帧之间处理的事情太多了,超过了 16.67ms,要优化每一帧的时间,又需要完整地知道浏览器在每一帧干了什么,这个就又涉及到了老生常谈的浏览器渲染页面。

到今天,虽然不同浏览器的渲染过程不完全相同,但是基本上大同小异,基本上都是:

简化一下也就是这个图:

这两张图,你可以在非常多不同的文章中看到。

回归本文的重点,Web 动画很大一部分开销在于层的重绘,以层为基础的复合模型对渲染性能有着深远的影响。当不需要绘制时,复合操作的开销可以忽略不计,因此在试着调试渲染性能问题时,首要目标就是要避免层的重绘。那么这就给动画的性能优化提供了方向,减少元素的重绘与回流。

这其中,如何减少页面的回流与重绘呢,这里就会运用到我们常说的** GPU 加速**。

GPU 加速的本质其实是减少浏览器渲染页面每一帧过程中的 reflow 和 repaint,其根本,就是让需要进行动画的元素,生成自己的 GraphicsLayer。

浏览器渲染一个页面时,它使用了许多没有暴露给开发者的中间表现形式,其中最重要的结构便是层(layer)。

在 Chrome 中,存在有不同类型的层:RenderLayer(负责 DOM 子树),GraphicsLayer(负责 RenderLayer 的子树)。

GraphicsLayer ,它对于我们的 Web 动画而言非常重要,通常,Chrome 会将一个层的内容在作为纹理上传到 GPU 前先绘制(paint)进一个位图中。如果内容不会改变,那么就没有必要重绘(repaint)层。

而当元素生成了自己的 GraphicsLayer 之后,在动画过程中,Chrome 并不会始终重绘整个层,它会尝试智能地去重绘 DOM 中失效的部分,也就是发生动画的部分,在 Composite 之前,页面是处于一种分层状态,借助 GPU,浏览器仅仅在每一帧对生成了自己独立 GraphicsLayer 元素层进行重绘,如此,大大的降低了整个页面重排重绘的开销,提升了页面渲染的效率。

因此,CSS 动画(Web 动画同理)优化的第一条准则就是让需要动画的元素生成了自己独立的 GraphicsLayer,强制开始 GPU 加速,而我们需要知道是,GPU 加速的本质是利用让元素生成了自己独立的 GraphicsLayer,降低了页面在渲染过程中重绘重排的开销。

当然,生成自己的独立的 GraphicsLayer,不仅仅只有 transform3d api,还有非常多的方式。在 CSS 中,包括但不限于(找了很多文档,没有很全面的,需要一个一个去尝试,通过开启 Chrome 的 Layer border 选项):

  • 3D 或透视变换(perspective、transform) CSS 属性

  • 使用加速视频解码的

  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 元素

  • 混合插件(如 Flash)

  • 对自己的 opacity 做 CSS 动画或使用一个动画变换的元素

  • 拥有加速 CSS 过滤器的元素

  • 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)

  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

对于上述一大段非常绕的内容,你可以再看看这几篇文章:

  • 【Web动画】CSS3 3D 行星运转 && 浏览器渲染原理

  • Accelerated Rendering in Chrome

除了上述准则之外,还有一些提升 CSS 动画性能的建议:

减少使用耗性能样式

不同样式在消耗性能方面是不同的,改变一些属性的开销比改变其他属性要多,因此更可能使动画卡顿。

例如,与改变元素的文本颜色相比,改变元素的 box-shadow 将需要开销大很多的绘图操作。box-shadow 属性,从渲染角度来讲十分耗性能,原因就是与其他样式相比,它们的绘制代码执行时间过长。这就是说,如果一个耗性能严重的样式经常需要重绘,那么你就会遇到性能问题。

类似的还有 CSS 3D 变换、mix-blend-mode、filter,这些样式相比其他一些简单的操作,会更加的消耗性能。我们应该尽可能的在动画过程中降低其使用的频率或者寻找替代方案。

当然,没有不变的事情,在今天性能很差的样式,可能明天就被优化,并且浏览器之间也存在差异。

因此关键在于,我们需要针对每一起卡顿的例子,借助开发工具来分辨出性能瓶颈所在,然后设法减少浏览器的工作量。学会 Chrome 开发者工具的 Performance 面板及其他渲染相关的面板非常重要,当然这不是本文的重点。大家可以自行探索。

使用 will-change 提高页面滚动、动画等渲染性能

will-change 为 Web 开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。

值得注意的是,用好这个属性并不是很容易:

不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。

有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。

不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。

给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

有人说 will-change 是良药,也有人说是毒药,在具体使用的时候,可以多测试一下。

最后

好了,本文从多个方面,由浅入深地描述了 CSS 动画我认为的一些比较重要、值得一讲、需要注意的点。当然很多地方点到即止,或者限于篇幅没有完全展开,很多细节还需要读者进一步阅读规范或者自行尝试验证,实践出真知,纸上得来终觉浅。

参考资料

关于本文作者:@SbCo原文:https://mp.weixin.qq.com/s/Ul3Es4N5zeQZ46zqrBlhYQ

HTML+CSS代码规范

Code Guide

编写一致、灵活和可持续的 HTML 和 CSS 代码的规范。

黄金定律

永远遵循同一套编码规范 — 可以是这里列出的,也可以是你自己总结的。

不管有多少人共同参与同一项目,一定要确保每一行代码都像是同一个人编写的。

HTML

语法

  • 标签不要大写,即便是 doctype 标签。

  • 用两个空格来代替制表符(tab) — 这是唯一能保证在所有环境下获得一致展现的方法。

  • 嵌套元素应当缩进一次(即两个空格)。

  • 对于属性的定义,永远全部使用双引号,绝不要使用单引号。

  • 不要在自闭合(self-closing)元素的尾部添加斜线 — HTML5 规范 中明确说明斜线是可忽略的。

  • 不要省略可选的结束标签(closing tag)(例如,</li></body>)。

<!doctype html>
<html>
  <head>
    <title>Page title</title>
  </head>
  <body>
    <img src="images/company-logo.png" alt="Company">
    <h1 class="hello-world">Hello, world!</h1>
  </body>
</html>

HTML5 doctype

为每个 HTML 页面的第一行添加 standards mode(标准模式) 声明,这样能够确保在每个浏览器中拥有一致的展现。

<!doctype html>
<html>
  <head>
  </head>
</html>

语言属性

根据 HTML5 规范:

强烈建议为 html 根元素指定 lang 属性,从而为文档设置正确的语言。这将有助于语音合成工具确定其所应该采用的发音,有助于翻译工具确定其翻译时所应遵守的规则等等。

更多关于 lang 属性的知识可以从 此规范 中了解。Sitepoint 站点上 给出了一份语言代码表

<html lang="en">
  <!-- ... -->
</html>

IE 兼容模式

IE 支持通过特定的 <meta> 标签来确定绘制当前页面所应该采用的 IE 版本。除非有强烈的特殊需求,否则最好是设置为 edge mode,从而通知 IE 采用其所支持的最新的绘制模式。

了解更多信息请 阅读这篇 Stack Overflow 上的文章

<meta http-equiv="x-ua-compatible" content="ie=edge">

字符编码

通过明确声明字符编码,能够确保浏览器快速并容易的判断页面内容的渲染方式。这样做的好处是,可以避免在 HTML 中使用字符实体标记(character entity),从而全部与文档编码一致(一般采用 UTF-8 编码)。

<head>
  <meta charset="UTF-8">
</head>

引入 CSS 和 JavaScript 文件

根据 HTML5 规范,在引入 CSS 和 JavaScript 文件时一般不需要指定 type 属性,因为 text/csstext/javascript 分别是它们的默认值。

HTML5 spec links

<!--External CSS-->
<link rel="stylesheet" href="code-guide.css">
​
<!-- In-document CSS -->
<style>
  /* ... */
</style>
​
<!-- JavaScript -->
<script src="code-guide.js"></script>

实用为王

尽量遵循 HTML 标准和语义,但是不要以牺牲实用性为代价。任何时候都要尽量使用最少的标签并保持最小的复杂度。

属性顺序

HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读性。

  • class

  • id, name

  • data-*

  • src, for, type, href, value

  • title, alt

  • role, aria-*

class 用于标识高度可复用组件,因此应该排在首位。id 用于标识具体组件,应当谨慎使用(例如,页面内的书签),因此排在第二位。

<a class="..." id="..." data-toggle="modal" href="#">
  Example link
</a><input class="form-control" type="text"><img src="..." alt="...">

布尔(boolean)型属性

布尔型属性可以在声明时不赋值。XHTML 规范要求为其赋值,但是 HTML5 规范不需要。

更多信息请参考 WhatWG section on boolean attributes

元素的布尔型属性如果有值,就是 true,如果没有值,就是 false。

如果一定要为其赋值的话,请参考 WhatWG 规范:

如果属性存在,其值必须是空字符串或 [...] 属性的规范名称,并且不要在首尾添加空白符。

简单来说,就是不用赋值。

<input type="text" disabled><input type="checkbox" value="1" checked><select>
  <option value="1" selected>1</option>
</select>

减少标签的数量

编写 HTML 代码时,尽量避免多余的父元素。很多时候,这需要迭代和重构来实现。请看下面的案例:

<!-- Not so great -->
<span class="avatar">
  <img src="...">
</span><!-- Better -->
<img class="avatar" src="...">

JavaScript 生成的标签

通过 JavaScript 生成的标签让内容变得不易查找、编辑,并且降低性能。能避免时尽量避免。

CSS

语法

  • 用两个空格来代替制表符(tab) — 这是唯一能保证在所有环境下获得一致展现的方法。

  • 为选择器分组时,将单独的选择器单独放在一行。

  • 为了代码的易读性,在每个声明块的左花括号前添加一个空格。

  • 声明块的右花括号应当单独成行。

  • 每条声明语句的 : 后应该插入一个空格。

  • 为了获得更准确的错误报告,每条声明都应该独占一行。

  • 所有声明语句都应当以分号结尾。最后一条声明语句后面的分号是可选的,但是,如果省略这个分号,你的代码可能更易出错。

  • 对于以逗号分隔的属性值,每个逗号后面都应该插入一个空格(例如,box-shadow)。

  • 不要在 rgb()rgba()hsl()hsla()rect() 值的内部的逗号后面插入空格。这样利于从多个属性值(既加逗号也加空格)中区分多个颜色值(只加逗号,不加空格)。

  • 对于属性值或颜色参数,省略小于 1 的小数前面的 0 (例如,.5 代替 0.5-.5px 代替 -0.5px)。

  • 十六进制值应该全部小写,例如,#fff。在扫描文档时,小写字符易于分辨,因为他们的形式更易于区分。

  • 尽量使用简写形式的十六进制值,例如,用 #fff 代替 #ffffff

  • 为选择器中的属性添加双引号,例如,input[type="text"]只有在某些情况下是可选的,但是,为了代码的一致性,建议都加上双引号。

  • 避免为 0 值指定单位,例如,用 margin: 0; 代替 margin: 0px;

对于这里用到的术语有疑问吗?请参考 Wikipedia 上的 syntax section of the Cascading Style Sheets article

/* Bad CSS */
.selector, .selector-secondary, .selector[type=text] {
  padding:15px;
  margin:0px 0px 15px;
  background-color:rgba(0, 0, 0, 0.5);
  box-shadow:0px 1px 2px #CCC,inset 0 1px 0 #FFFFFF
}
​
/* Good CSS */
.selector,
.selector-secondary,
.selector[type="text"] {
  padding: 15px;
  margin-bottom: 15px;
  background-color: rgba(0,0,0,.5);
  box-shadow: 0 1px 2px #ccc, inset 0 1px 0 #fff;
}

声明顺序

相关的属性声明应当归为一组,并按照下面的顺序排列:

  1. Positioning

  2. Box model

  3. Typographic

  4. Visual

  5. Misc

由于定位(positioning)可以从正常的文档流中移除元素,并且还能覆盖盒模型(box model)相关的样式,因此排在首位。盒模型排在第二位,因为它决定了组件的尺寸和位置。

其他属性只是影响组件的 内部 或者是不影响前两组属性,因此排在后面。

完整的属性列表及其排列顺序请参考 Bootstrap property order for Stylelint

.declaration-order {
  /* Positioning */
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 100;
​
  /* Box-model */
  display: block;
  float: right;
  width: 100px;
  height: 100px;
​
  /* Typography */
  font: normal 13px "Helvetica Neue", sans-serif;
  line-height: 1.5;
  color: #333;
  text-align: center;
​
  /* Visual */
  background-color: #f5f5f5;
  border: 1px solid #e5e5e5;
  border-radius: 3px;
​
  /* Misc */
  opacity: 1;
}

不要使用@import

<link> 标签相比,@import 指令要慢很多,不光增加了额外的请求次数,还会导致不可预料的问题。替代办法有以下几种:

  • 使用多个 <link> 元素

  • 通过 SassLess 之类的 CSS 预处理器将多个 CSS 文件编译为一个文件

  • 通过 Rails、Jekyll 或其他系统中提供过 CSS 文件合并功能

请参考 Steve Souders 的文章了解更多知识。

<!-- Use link elements -->
<link rel="stylesheet" href="core.css"><!-- Avoid @imports -->
<style>
  @import url("more.css");
</style>

媒体查询(Media query)的位置

将媒体查询放在尽可能相关规则的附近。不要将他们打包放在一个单一样式文件中或者放在文档底部。如果你把他们分开了,将来只会被大家遗忘。下面给出一个典型的实例。

.element { ... }
.element-avatar { ... }
.element-selected { ... }
​
@media (min-width: 480px) {
  .element { ...}
  .element-avatar { ... }
  .element-selected { ... }
}

带前缀的属性

当使用特定厂商的带有前缀的属性时,通过缩进的方式,让每个属性的值在垂直方向对齐,这样便于多行编辑。

在 Textmate 中,使用 Text → Edit Each Line in Selection (⌃⌘A)。在 Sublime Text 2 中,使用 Selection → Add Previous Line (⌃⇧↑) 和 Selection → Add Next Line (⌃⇧↓)。

/* Prefixed properties */
.selector {
  -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
          box-shadow: 0 1px 2px rgba(0,0,0,.15);
}

单行规则声明

对于只包含一条声明的样式,为了易读性和便于快速编辑,建议将语句放在同一行。对于带有多条声明的样式,还是应当将声明分为多行。

这样做的关键因素是为了错误检测 — 例如,CSS 校验器指出在 183 行有语法错误。如果是单行单条声明,你就不会忽略这个错误;如果是单行多条声明的话,你就要仔细分析避免漏掉错误了。

/* Single declarations on one line */
.span1 { width: 60px; }
.span2 { width: 140px; }
.span3 { width: 220px; }
​
/* Multiple declarations, one per line */
.sprite {
  display: inline-block;
  width: 16px;
  height: 15px;
  background-image: url("../img/sprite.png");
}
.icon           { background-position: 0 0; }
.icon-home      { background-position: 0 -20px; }
.icon-account   { background-position: 0 -40px; }

简写形式的属性声明

在需要显示地设置所有值的情况下,应当尽量限制使用简写形式的属性声明。常被滥用的简写属性如下:

  • padding

  • margin

  • font

  • background

  • border

  • border-radius

大部分情况下,我们不需要为简写形式的属性声明指定所有值。例如,HTML 的标题元素只需要设置上、下边距(margin)的值,因此,在必要的时候,只需覆盖这两个值就可以了。0 值表示对浏览器默认值或以前指定的值的覆盖。

过多地使用属性的简写形式会导致代码出现不必要的覆盖和意外的副作用。

在 MDN(Mozilla Developer Network)上一篇非常好的关于 shorthand properties 的文章,对于不太熟悉简写属性声明及其行为的用户很有用。

/* Bad example */
.element {
  margin: 0 0 10px;
  background: red;
  background: url("image.jpg");
  border-radius: 3px 3px 0 0;
}
​
/* Good example */
.element {
  margin-bottom: 10px;
  background-color: red;
  background-image: url("image.jpg");
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
}

Less 和 Sass 中的嵌套

避免不必要的嵌套。这是因为虽然你可以使用嵌套,但是并不意味着应该使用嵌套。只有在必须将样式限制在父元素内(也就是后代选择器),并且存在多个需要嵌套的元素时才使用嵌套。

扩展阅读:

// Without nesting
.table > thead > tr > th { … }
.table > thead > tr > td { … }
​
// With nesting
.table > thead > tr {
  > th { … }
  > td { … }
}

Less 和 Sass 中的操作符

为了提高可读性,在圆括号中的数学计算表达式的数值、变量和操作符之间均添加一个空格。

// Bad example
.element {
  margin: 10px 0 @variable*2 10px;
}
​
// Good example
.element {
  margin: 10px 0 (@variable * 2) 10px;
}

注释

代码是由人编写并维护的。请确保你的代码能够自描述、注释良好并且易于他人理解。好的代码注释能够传达上下文关系和代码目的。不要简单地重申组件或 class 名称。

对于较长的注释,务必书写完整的句子;对于一般性注解,可以书写简洁的短语。

/* Bad example */
/* Modal header */
.modal-header {
  ...
}
​
/* Good example */
/* Wrapping element for .modal-title and .modal-close */
.modal-header {
  ...
}

class 命名

  • class 名称中只能出现小写字符和破折号(dashe)(不是下划线,也不是驼峰命名法)。破折号应当用于相关 class 的命名(类似于命名空间)(例如,.btn.btn-danger)。

  • 避免过度任意的简写。.btn 代表 button,但是 .s 不能表达任何意思。

  • class 名称应当尽可能短,并且意义明确。

  • 使用有意义的名称。使用有组织的或目的明确的名称,不要使用表现形式(presentational)的名称。

  • 基于最近的父 class 或基本(base) class 作为新 class 的前缀。

  • 使用 .js-* class 来标识行为(与样式相对),并且不要将这些 class 包含到 CSS 文件中。

在为 Sass 和 Less 变量命名时也可以参考上面列出的各项规范。

/* Bad example */
.t { ... }
.red { ... }
.header { ... }
​
/* Good example */
.tweet { ... }
.important { ... }
.tweet-header { ... }

选择器

  • 对于通用元素使用 class ,这样利于渲染性能的优化。

  • 对于经常出现的组件,避免使用属性选择器(例如,[class^="..."])。浏览器的性能会受到这些因素的影响。

  • 选择器要尽可能短,并且尽量限制组成选择器的元素个数,建议不要超过 3 。

  • 只有在必要的时候才将 class 限制在最近的父元素内(也就是后代选择器)(例如,不使用带前缀的 class 时 — 前缀类似于命名空间)。

扩展阅读:

/* Bad example */
span { ... }
.page-container #stream .stream-item .tweet .tweet-header .username { ... }
.avatar { ... }
​
/* Good example */
.avatar { ... }
.tweet-header .username { ... }
.tweet .avatar { ... }

代码组织

  • 以组件为单位组织代码段。

  • 制定一致的注释规范。

  • 使用一致的空白符将代码分隔成块,这样利于扫描较大的文档。

  • 如果使用了多个 CSS 文件,将其按照组件而非页面的形式分拆,因为页面会被重组,而组件只会被移动。

/*
 * Component section heading
 */.element { ... }
​
​
/*
 * Component section heading
 *
 * Sometimes you need to include optional context for the entire component. Do that up here if it's important enough.
 */.element { ... }
​
/* Contextual sub-component or modifer */
.element-heading { ... }

编辑器配置

将你的编辑器按照下面的配置进行设置,以避免常见的代码不一致和差异:

  • 用两个空格代替制表符(soft-tab 即用空格代表 tab 符)。

  • 保存文件时,删除尾部的空白符。

  • 设置文件编码为 UTF-8。

  • 在文件结尾添加一个空白行。

参照文档并将这些配置信息添加到项目的 .editorconfig 文件中。例如:Bootstrap 中的 .editorconfig 实例。更多信息请参考 about EditorConfig

来源:https://codeguide.bootcss.com/

CSS选择器 (Selector )

Selector 语法:

选择器语法 说明 部分 等级
* 任何元素 § 5.2 通用选择器 2
E E 型元素 § 5.1 类型(标签名称)选择器 1
E:not(s1, s2, …) 与复合选择器 s1或复合选择器 s2都不匹配的 E 元素 § 4.3 否定(无匹配)伪类::not() 3/4
E:is(s1, s2, …) 匹配复合选择器 s1和/或复合选择器 s2的 E 元素 § 4.2 Matches-Any Pseudo-class: :is() 4
E:where(s1, s2, …) 匹配复合选择器 s1和/或复合选择器 s2但不提供特异性 的 E 元素。 § 4.4 特异性调整伪类::where() 4
E:has(rs1, rs2, …) 一个 E 元素,如果相对选择器 rs1或rs2中的任何一个,当使用 E 作为:scope 元素进行评估时,匹配一个元素 § 4.5 关系伪类::has() 4
E.warning 属于类的 E 元素warning(文档语言指定如何确定类)。 § 6.6 类选择器 1
E#myid ID 等于 的 E 元素myid § 6.7 ID 选择器 1
E[foo] 具有foo属性 的 E 元素 § 6.1 属性存在和值选择器 2
E[foo="bar"] 一个 E 元素,其foo属性值正好等于bar § 6.1 属性存在和值选择器 2
E[foo="bar" i] 一个 E 元素,其foo属性值完全等于任何(ASCII 范围)大小写排列bar § 6.3 区分大小写 4
E[foo="bar" s] 一个 E 元素,其foo属性值等于bar § 6.3 区分大小写 4
E[foo~="bar"] 一个 E 元素,其foo属性值是由空格分隔的值的列表,其中一个值完全等于bar § 6.1 属性存在和值选择器 2
E[foo^="bar"] 一个 E 元素,其foo属性值正好以字符串开头bar § 6.2 子串匹配属性选择器 3
E[foo$="bar"] 一个 E 元素,其foo属性值正好以字符串结尾bar § 6.2 子串匹配属性选择器 3
E[foo*="bar"] 一个 E 元素,其foo属性值包含子字符串bar § 6.2 子串匹配属性选择器 3
E[foo|="en"] 一个 E 元素,其foo属性值是以连字符分隔的值列表en § 6.1 属性存在和值选择器 2
E:dir(ltr) 具有从左到右方向性的 E 类型元素(文档语言指定如何确定方向性) § 7.1 方向性伪类::dir() 4
E:lang(zh, "*-hant") 一个 E 类型的元素,标记为中文(任何方言或书写系统)或以其他方式用繁体汉字书写 § 7.2 语言伪类::lang() 2/4
E:any-link 一个 E 元素是超链接的源锚 § 8.1 超链接伪类: :any-link 4
E:link 一个 E 元素是目标尚未访问的超链接的源锚点 § 8.2 链接历史伪类::link 和 :visited 1
E:visited 一个 E 元素是一个超链接的源锚,其目标已经被访问过 § 8.2 链接历史伪类::link 和 :visited 1
E:local-link E 元素是指向当前 URL 的超链接的源锚 § 8.3 本地链接伪类: :local-link 4
E:target E 元素是当前 URL 的目标 § 8.4 目标伪类: :target 3
E:target-within 一个 E 元素,它是当前 URL 的目标,或者包含一个这样做的元素。 § 8.5 目标容器伪类::target-within 4
E:scope 一个 E 元素是一个指定的参考元素 § 8.6 参考元素伪类::scope 4
E:current 当前呈现在时间维度画布中的 E 元素 § 10.1 当前元素伪类: :current 4
E:current(s) 最深的 E 元素:匹配选择器s 的当前元素 § 10.1 当前元素伪类: :current 4
E:past 过去在时间维度画布中的 E 元素 § 10.2 过去元素伪类::past 4
E:future 时间维度画布中未来的 E 元素 § 10.3 未来元素伪类: :future 4
E:active 处于激活状态的 E 元素 § 9.2 激活伪类: :active 1
E:hover 光标下的 E 元素,或光标下有后代的 E 元素 § 9.1 指针悬停伪类::hover 2
E:focus 具有用户输入焦点的 E 元素 § 9.3 输入焦点伪类: :focus 2
E:focus-within 具有用户输入焦点或包含具有输入焦点的元素的 E 元素。 § 9.5 焦点容器伪类::focus-within 4
E:focus-visible 具有用户输入焦点的 E 元素,并且 UA 已确定应为该元素绘制焦点环或其他指示符 § 9.4 焦点指示伪类::focus-visible 4
E:enabledE:disabled 分别启用或禁用的用户界面元素 E § 12.1.1 :enabled 和 :disabled 伪类 3
E:read-writeE:read-only 用户可更改或不可更改的用户界面元素 E § 12.1.2 可变性伪类::read-only 和 :read-write 3-UI/4
E:placeholder-shown 当前显示占位符文本的输入控件 § 12.1.3 占位符显示的伪类::placeholder-shown 3-UI/4
E:default 用户界面元素 E,它是一组相关选项中的默认项 § 12.1.4 默认选项伪类::默认 3-UI/4
E:checked 选中/选中的用户界面元素 E(例如单选按钮或复选框) § 12.2.1 选择选项伪类: :checked 3
E:indeterminate 处于不确定状态(既未选中也未选中)的用户界面元素 E § 12.2.2 不确定值伪类: :indeterminate 4
E:validE:invalid 满足或不满足其数据有效性语义的用户输入元素 E § 12.3.2 有效性伪类::valid 和 :invalid 3-UI/4
E:in-rangeE:out-of-range 用户输入元素 E,其值在范围内/范围外 § 12.3.3 范围伪类::in-range 和 :out-of-range 3-UI/4
E:requiredE:optional 需要/不需要输入的用户输入元素 E § 12.3.4 可选性伪类::required 和 :optional 3-UI/4
E:blank 值为空白的用户输入元素 E(空/缺失) § 12.3.1 空值伪类: :blank 4
E:user-invalid 用户更改的用户输入元素 E 输入不正确(无效、超出范围、省略但需要) § 12.3.5 用户交互伪类::user-valid 和 :user-invalid 4
E:root 一个 E 元素,文档的根 § 13.1:根伪类 3
E:empty 一个没有子元素(既没有元素也没有文本)的 E 元素,可能除了空白 § 13.2:空的伪类 3
E:nth-child(n [of S]?) 一个 E 元素,其父元素匹配S的第n个子元素 § 13.3.1 :nth-child() 伪类 3/4
E:nth-last-child(n [of S]?) 一个 E 元素,其父元素匹配S的第n个子元素,从最后一个元素开始计数 § 13.3.2 :nth-last-child() 伪类 3/4
E:first-child 一个 E 元素,其父元素的第一个子元素 § 13.3.3 :first-child 伪类 2
E:last-child 一个 E 元素,其父元素的最后一个子元素 § 13.3.4 :last-child 伪类 3
E:only-child 一个 E 元素,其父元素的唯一子元素 § 13.3.5:独生子伪类 3
E:nth-of-type(n) 一个 E 元素,其类型的第n个兄弟 元素 § 13.4.1 :nth-of-type() 伪类 3
E:nth-last-of-type(n) 一个 E 元素,其类型的第n个兄弟元素,从最后一个开始计数 § 13.4.2 :nth-last-of-type() 伪类 3
E:first-of-type 一个 E 元素,其类型的第一个兄弟元素 § 13.4.3:第一类伪类 3
E:last-of-type 一个 E 元素,其类型的最后一个兄弟元素 § 13.4.4 :last-of-type 伪类 3
E:only-of-type 一个 E 元素,只有其类型的同级元素 § 13.4.5 :only-of-type 伪类 3
E F E 元素的 F 元素后代 § 14.1 后代组合子 ( ) 1
E > F E 元素的 F 元素子元素 § 14.2 子组合子 (>) 2
E + F 紧跟在 E 元素之前的 F 元素 § 14.3 下一个兄弟组合子 (+) 2
E ~ F 前面有一个 E 元素的 F 元素 § 14.4 后续兄弟组合子 (~) 3
F || E 一个 E 元素,表示网格/表格中的一个单元格,该单元格属于由元素 F 表示的列 § 15.1 列组合符 (||) 4
E:nth-col(n) 一个 E 元素,表示属于网格/表格中第n列的 单元格 § 15.2 :nth-col() 伪类 4
E:nth-last-col(n) 一个 E 元素,表示属于网格/表格中第n列的单元格,从最后一个开始计数 § 15.3 :nth-last-col() 伪类 4

注意:[CSS3UI]中引入了一些 4 级选择器(上面称为“3-UI”)。

来源:https://drafts.csswg.org/selectors-4