图解CSS:CSS阴影

发布于 大漠

在现代 Web 中,阴影已经成为主要设计要素之一,甚至是随处可见。 阴影增加了质感、透视、并强调物体的尺寸。在 Web 设计中,使用光和影子可以增加物理上的真实感,并且可以用来制作丰富的、可触摸的 UI 界面。从 CSS 技术角度来说,在 Web 中给 UI 添加阴影效果有多种不同的技术方案,但从实现阴影效果的 CSS 属性来说,常见的主要有 text-shadow(文本阴影)、box-shadow(盒子阴影)和 filterdrop-shadow()(不规则阴影)。今天,我们就一起来聊聊 CSS 这方面的特性。

有关于阴影的一些基础理论

在开始聊有关于阴影方面的 CSS 属性之前,我们先来花一些时间和篇幅来聊与阴影相关的理论基础。

光和阴影

从现实生活中,我们可以轻易地发现,光可以产生阴影。也就是说,谈论阴影不可能不涉及到光。光可以控制影子的方向以及影子出现的深浅程度。简单地说,光和阴影,两者缺一不可

光和阴影是一门复杂的学科,这已超出了我所掌握的知识领域,如果你想更深入的探究光和阴影之间的关系,可以阅读 @BCiechanowski 的《Lights and Shadows》一文。该文深入的阐述了光和阴影相关的知识!

在 Web UI 设计中,**Google 的 Material Design 设计系统**是有效运用光和阴影的最典型案例。我想你肯定感受到了 Material Design 中光和阴影带来的美感,因为在 Google 的产品上几乎都能看到光和阴影带来的设计美感:

Material Design 设计系统以物理世界为线索,利用光线、表面和投射的阴影在三维空间中表达 Web UI 界面。在这个设计系统中,虚拟灯光照亮了用户界面。关键的灯光创造出更清晰的,有方向性的阴影,称为 关键阴影。环境光从各个角度出现,创造出扩散的,柔和的阴影,称为环境阴影

除了 Material Design 这种经典案例之外,光和阴影在一些新的 UI 设计中也起着关键性的作用,比如 Glassmorphism UINeumorphism UI

当苹果公司推出 iOS8 时,它提高了应用设计的标准,特别是在屏幕效果方面。最重要的变化之一是在整个过程中使用了模糊效果,最明显的是在控制中心;当你从屏幕的底部边缘向上滑动时,你会发现控制中心,而背景是模糊的。这种模糊(也称为磨砂效果)是以一种互动的方式发生的,因为你完全用手指的移动来控制它。

特别是在最新版本的 iOS 系统中。当用户的手按在手电筒、相机、计算器和计时器等图标上,实时模糊效果就会发生:

在这些设计中,光线仍然是一个重要的影响因素,因为它允许元素融毛主席桌面,甚至融入用户界面的其他面板。同样,在你的界面中采用这种方式也是一种设计选择。无论怎样,你都可以看到光线是如何影响深度的视觉感知的。

光源和颜色

现在我们对光和阴影之前的关系有了一个最基础的认识,接下来可以稍微再深入一点点。看看光是如何影响阴影的。我们已经看到了光的强度如何在不同的深度产生阴影。但是关于光如何影响阴影的方向和颜色,还是有很多东西可以聊的。

当光照在一个物体上时,有两种阴影出现,一种是投影(Drop Shadow 也称 Cast Shadow),另一种是形体阴影(Form Shadow):

投影

当一个物体阻挡了光源时,就会产生水滴状阴影。阴影可以有不同的色调和值。色彩术语繁多,令人感到困惑,这里就简单地说一下色调和值。

色调是一种与灰色相混合的色相。值描述了一种颜色的整体明度或暗度。在绘画中,值是一个很大的问题,因为它是艺术家如何将光线和物体的关系转化为颜色的。

在 Web 设计软件中(比如 Sketch、Figma),有关于颜色的这些描述和定义在颜色选择器面板中可以看到:

而在 CSS 描述和定义颜色也有多种方式,如果你对这方面感兴趣的话,可以移步阅读下面这几篇有关于颜色方面的文章:

形体阴影

另一方面,形体阴影是物体远离光源的一面。形体阴影比投影的边缘更柔和、更不明确。形体阴影说明了一个物体的体积和深度。

阴影的外观取决于光的方向、光的强度,以及物体和投射阴影的表面之间的距离。光线越强,影子就越暗、越清晰。光线越柔和(弱),影子就越微弱、越柔和。在某些情况下,对于定向光,我们会得到两个不同的影子。暗影(Umbra)是光线被阻挡的地方,半影(Penumbra)是光线被投下的地方。

如果一个表面靠近一个物体,影子会更清晰;反之,影子会比较模糊。这在我们生活中时常能看到:

光也可能从物体的侧面或另一表面反射出来。亮的一面会反射光线,反之暗的一面会吸收光线。

这些有关于光的概念和理论对于 Web 设计来说是非常重要,也非常有阶值的。光学背后的物理学是一个复杂的话题,上面提到的仅是一些基础性的概念。如果你想看看基于不同光源投射出的阴影的效果的话,可以浏览 @drawinghowto 绘制的一些关于光和阴影相关的漫画图:

光源的定位

阴影和光源是相辅相成的,所以,光源的位置对阴影的效果起着决定性的作用。光源位置的不同,产生阴影的效果也将不同。假如光源的位置在一个实物的上方,那么阴影将会在该实物的下方,反之,阴影将会在这个实物的上方。具体的如下图所示:

在 Web 中,一个元素就相当于是一个实物,如果这个元素的四边(甚至包括四个顶角)都有光源,那这个元素将不会有任何阴影的产生;如果这个元素只有顶部有光源,那这个元素的底部就会产生阴影:

实际上,光源可以投射在实物的任何方向(位置),只不过在同一个设计中,为了让阴影的效果保持一致,需要让页面上的所有元素产生的阴影保持一致。也就是说,一个元素的阴影与页面上其他的阴影相匹配。

立视图

阴影的最大优势可以让物体增加实质感。换句话说,阴影也可以传达海拔高度,有立视图的感觉。这个特征在 Material Design 中表现的活灵涛现,Material Design 中的设计展示了阴影是如何被用来创造元素之间可感知的分离。如下图所示:

内阴影 vs. 外阴影

就阴影而言,他可分为 外阴影内阴影。在 Web UI 中内阴影相对而言是较少见的。它的参数与外阴影相同,但它出现在实物的内部,可以达到下沉的效果。

相对于外阴影来说,它并怎么流行(用得较少),主要是因为大多数的 Web UI 界面是多个面层叠在一起的。基于这个因素,外阴影相对来说更有意义,因为它提供了深度(海拔)。内阴影会暗示物体上有一个洞,就会破坏堆栈的视觉结构。

内阴影的风格使用较多的场景是在表单控件上,比如输入框(包括单选按钮和复选框等)。另外,要是你接触过 Neumorphism UI 的话,你会发现,这种风格的 UI 有一个最大的特征就是使用内阴影设计,达到一种类似挤压的UI效果。

Neumorphism UI 的主要问题是使用内阴影和挤压的形状作为“选中”状态的概念。默认状态和选中状态之间可感知的差异非常小,甚至非视力障碍的用户有时也会完全忽略它。这也反过来导致了 Neumorphism UI 最大的缺陷,可访问性很大差

阴影的分层

Web UI 中的阴影和现实中的阴影是相似的,同一个元素不局限于只有一个阴影。就 CSS 中的阴影来说,也是的,不管是 text-shadowbox-shadow 还是 drop-shadow() 都可以同时存在多个阴影:

简单地说,就是在阴影的属性中使用逗号(,) 分割(放置)多个描述阴影的值。使用多层阴影,可以构建一些 3D 的 UI 效果:

阴影的模糊度

现在的大多数设计软件都有一个高斯模糊的设置,比如 Sketch 中的高斯模糊:

正如上图所示,除了高斯模糊(Guassian Blur)之外,还有动感模糊(Motion Blur)、缩放模糊(Zoom Blur)和背景模糊(Background Blur):

比如前面提到的 iOS 中的模糊效果、Glassmorphism UI(也称之为 Glass UI)和磨砂效果:

高斯模糊是最常见的模糊类型,但他和阴影并没有任何关系。

而在阴影中也存有模糊,可以帮助你在物体下产生非标准的,点状的阴影。只要模糊一个椭圆,并将它放在投射阴影的物体下面。你可以单独使用它,也可以将它与标准的投射阴影结合起来,获得更独特的效果。

正如上面所述,阴影是离不开光的,也正因此,也将其称为光影效果。前面介绍的是阴影中的一些概念和基础,但光和阴影是复杂的学科,实物的阴影受光源的类型、物体的明暗、光源的位置等因素的影响。正如 @卢维贤 在站酷分享的《了解设计中的光影视界》一文中文末提到的光影的相关知识图谱:

CSS 中的阴影

以我的愚见,一个好的 Web 页面或应用应该给用户有一个切实的“真实”的质感。在 Web 中实现这些质感有很多因素,阴影就是其中一个关键因素。

阴影能给元素增加真实的质感、透视、并强调元素的尺寸。简单地说,可以增加物理上的真实感,让人有一种丰富的、可触摸的 UI 界面!

在 Web UI 的开发中,实现阴影效果就需要用到 CSS 技术。在 CSS 中,主要使用 text-shadowbox-shadowfilter中的drop-shadow() 来实现。

  • 文本阴影text-shadow 给文本添加阴影
  • 盒子阴影box-shadow 给元素盒子添加阴影
  • 不规则阴影:也投影,drop-shadow()可以给文本、盒子以及不规则形状添加阴影

文本阴影:text-shadow

先来看文本阴影,即 CSS 的 text-shadow 属性。

text-shadowCSS Text Decoration Module Level 3 规范中的一个属性。主要用来给 Web 中的文本添加阴影。该属性语法规则很简单:

text-shadow: none | [ <color>? && <length>{2,3} ]#

其默认值为 none,表示文本无阴影。

text-shadow 接受四个参数值,分别是:

  • <x-offset>x轴(或Inline Axis)位移值,该值是必选值。表示阴影在x轴相对于文本的偏移量,即水平偏移量。它的值可以是正负值,如果是正值,表示阴影向右偏移;如果是负值,表示阴影向左偏移
  • <y-offset>y轴(或Block Axis)位移值,该值也是必选值。表示阴影在y轴相对于文本的偏移量,即垂直偏移量。它的值可以是正负值,如果是正值,表示阴影向下偏移;如果是负值,表示阴影向上偏移
  • <blur>:可选值,如果该值未显式设置,其值为0。该值表示阴影模糊半径,其值越大,表示阴影模糊半径越大,阴影也就越大越淡
  • <color>:可选值,如果该值未显式设置,其值使用 UA(用户代理)自行选择的颜色

text-shadow的使用很简单:

.text-shadow {
    text-shadow: 5px 5px 5px #09faa0;
}

你可以尝试调整下面示例中运用于 text-shadow 属性上各参数,查看效果的变化:

text-shadow属性中的 x-offsety-offsetblur 三个参数的值是个<length>值(不接受百分比值)。相对而言,使用 em 单位要比其他单位更灵活一些,因为 em 单位会相对于元素的 font-size 计算。

CSS 的 text-shadow 可以接受以逗号(,)作为分隔的多个阴影,比如:

.multiple-text-shadow {
    text-shadow: 2px 2px 1px #09fa, 3px 3px 1px #90f;
}

使用多个阴影可以制作 3D 的文本效果:

.text-3d {
    text-shadow: 
        0.25px 0.25px #20666D, 
        0.5px 0.5px #20666D, 
        0.75px 0.75px #20666D, 
        1px 1px #20666D, 
        1.25px 1.25px #20666D, 
        1.5px 1.5px #20666D, 
        1.75px 1.75px #20666D, 
        2px 2px #20666D, 
        2.25px 2.25px #20666D, 
        2.5px 2.5px #20666D, 
        2.75px 2.75px #20666D, 
        3px 3px #20666D, 
        3.25px 3.25px #20666D, 
        3.5px 3.5px #20666D, 
        3.75px 3.75px #20666D, 
        4px 4px #20666D, 
        4.25px 4.25px #20666D, 
        4.5px 4.5px #20666D, 
        4.75px 4.75px #20666D, 
        5px 5px #20666D, 
        5.25px 5.25px #20666D, 
        5.5px 5.5px #20666D, 
        5.75px 5.75px #20666D, 
        6px 6px #20666D;
}

效果如下:

也可以使用多个text-shadow来模拟文本描边的效果:

.text-stroke {
    text-shadow: 
        0px -6px 0 #212121, 
        0px -6px 0 #212121, 
        0px 6px 0 #212121,
        0px 6px 0 #212121, 
        -6px 0px 0 #212121, 
        6px 0px 0 #212121, 
        -6px 0px 0 #212121,
        6px 0px 0 #212121, 
        -6px -6px 0 #212121, 
        6px -6px 0 #212121,
        -6px 6px 0 #212121, 
        6px 6px 0 #212121, 
        -6px 18px 0 #212121,
        0px 18px 0 #212121, 
        6px 18px 0 #212121, 
        0 19px 1px rgb(0 0 0 / 0.1),
        0 0 6px rgb(0 0 0 / 0.1), 
        0 6px 3px rgb(0 0 0 / 0.3),
        0 12px 6px rgb(0 0 0 / 0.2), 
        0 18px 18px rgb(0 0 0 / 0.25),
        0 24px 24px rgb(0 0 0 / 0.2), 
        0 36px 36px rgb(0 0 0 / 0.15);
}

文本描边效果还可以使用 CSS 的 text-stroke 属性来实现,如果你对 text-stroke 感兴趣的话,可以阅读《text-stroke实现文本描边效果》一文。

发挥你的想象,你可以使用text-shadow创作出很多与众不同的效果:

盒子阴影:box-shadow

接下来,我们再来聊一下盒子阴影,即 CSS 的 box-shadow 属性。

从《Web布局: CSS 盒模型》和《视觉格式化模型》中可以得知,Web 中的每个元素(包括伪元素::before::after 生成的内容)都是一个盒子。这个盒子如果不使用 MaskingClipping 做额外处理的话,他们在视觉上都是一个矩形的盒子,如果需要给这盒子添加阴影,就需要使用 box-shadow 属性。

CSS 的 box-shadow 属性和 text-shadow 属性并不隶属于同一个模块中,box-shadow 属性是在 CSS Backgrounds and Borders Module Level 3 模块中定义的。它的语法规则和 text-shadow 非常相似:

box-shadow: none | <shadow>#
<shadow> = <color>? && [<length>{2} <length [0,∞]>? <length>?] && inset?

box-shadowtext-shadow属性有几个参数是相似的,即 <x-offset><y-offset><blur><color>,不同之处是,box-shadowtext-shadow 属性上新增了另外两个参数:

  • inset:是一个可选值,如果显示指定了该值,表示阴影是一个内阴影(阴影落在盒子内部,看起来就像是内容被压低了),阴影在边框之内(即使是透明边框)、背景之上、内容之下;如果未显式指定,阴影在边框外,即阴影向外扩散。box-shadow 之下不会显式设置inset
  • <spread>:可选值,和 <blur>类似,也是一个<length>值,默认值为0(即,阴影与元素同样大)。指阴影扩散半径,取值为正值时,阴影扩散(阴影放大);取负值时,阴影收缩。

box-shadow的使用和 text-shadow 很简单:

.outer-shadow {
    box-shadow: 5px 5px 5px 5px #09fa00;
}

.inner-shadow {
    box-shadow: inset 5px 5px 5px 5px #09fa00;
}

你可以尝试调整下面示例中运用于 box-shadow 属性上各参数,查看盒子阴影的变化:

box-shadow 属性值也可以使用逗号(,)分隔多个值,来创建多阴影效果:

.box-shadow {
    box-shadow: 
        rgb(240 46 170 / 0.4) 5px 5px, 
        rgb(240 46 170 / 0.3) 10px 10px, 
        rgb(240 46 170 / 0.2) 15px 15px, 
        rgb(240 46 170 / 0.1) 20px 20px, 
        rgb(240 46 170 / 0.05) 25px 25px;
}

利用多阴影的特性可以构建一些3D的效果:

在使用 box-shadow 时,只需调整其参数,可以实现很多阴影效果:

CSS的box-shadow除了能给元素添加阴影效果之外,还要以用来绘制视觉效果。也就是说,CSS 的 box-shadow 可以像 borderborder-radius 等属性一样,绘制 CSS 的视觉效果。比如 A Single Div 很多案例的效果都有 box-shadow 的影子:

有关于使用 CSS 绘制图形或者说使用 CSS 完成一些视觉效果更详细的介绍可以阅读《CSS的视觉效果》一文。

不过,box-shadow 要比 text-shadow 复杂的多,特别是细节方面。接下来,我们来聊一些只关于 box-shadow 方面的内容。

阴影的分层

正如前面所述,不管是 text-shadow 还是 box-shadow 都要以使用逗号(,)的方式分隔多个阴影属性的值来构建多个阴影。这样构建阴影的方式,每一个逗号隔开的值就是一层阴影,也就是说,阴影是可以分层的。就拿盒子阴影(box-shadow)来说,我们不使用单一的盒子阴影,而是将多个盒子阴影(box-shadow)堆叠在一起,并有稍微不同的偏移量(xy轴的值不同)和辐射(模糊半径和扩散半径的值不同)。比如下面这个示例,单层阴影和多层阴影的差异:

.shadow {
    box-shadow: 0 6px 6px hsl(0deg 0% 0% / 0.3);
}

.multiple-shadow {
    box-shadow:
        0 1px 1px hsl(0deg 0% 0% / 0.075),
        0 2px 2px hsl(0deg 0% 0% / 0.075),
        0 4px 4px hsl(0deg 0% 0% / 0.075),
        0 8px 8px hsl(0deg 0% 0% / 0.075),
        0 16px 16px hsl(0deg 0% 0% / 0.075)
}

通过对多个阴影进行分层,我们可以创造出一点现实生活中阴影的微妙效果。其中原理就是:通过改变box-shadow的偏移量(xy轴位置)、模糊半径、扩展半径和颜色来模糊真实世界中光线照射到一个物体上并投下阴影的微妙效果(真实效果)。当然,这种模拟并无法真正的表达现实生活中阴影复杂性和细微差别。

正是这种简单的分层技术让我们对阴影的渲染有了更多的控制,有了它,我们可以微调锐度(Sharpness)、距离(Distance)和扩散(Spread)。例如,你可以增加或减少阴影的数量来创造一个更小或更大的扩散。

.shadow {
    box-shadow: 
        0 1px 1px rgb(0 0 0 / 0.15), 
        0 2px 2px rgb(0 0 0 / 0.15), 
        0 4px 4px rgb(0 0 0 / 0.15), 
        0 8px 8px rgb(0 0 0 / 0.15);
}

.shadow {
    box-shadow: 
        0 1px 1px rgb(0 0 0 / 0.11), 
        0 2px 2px rgb(0 0 0 / 0.11), 
        0 4px 4px rgb(0 0 0 / 0.11), 
        0 8px 8px rgb(0 0 0 / 0.11), 
        0 16px 16px rgb(0 0 0 / 0.11), 
        0 32px 32px rgb(0 0 0 / 0.11);
}

上面示例中的两个盒子阴影,一个是有四层,另一个是六层。只不过,上面两个阴影并不是最佳的分层技术。也就是说,在多个盒子阴影的使用上,如果我们要更好的让阴影效果更接近真实生活中的阴影的话,还是需要做一些微小的处理。可以同时使用每个层的颜色透明度值和模糊半径来改变深度的阴影。比如:

.shadow-sharp {
    box-shadow: 
        0 1px 1px rgb(0 0 0 / 0.25), 
        0 2px 2px rgb(0 0 0 / 0.20), 
        0 4px 4px rgb(0 0 0 / 0.15), 
        0 8px 8px rgb(0 0 0 / 0.10),
        0 16px 16px rgb(0 0 0 / 0.05);
}

.shadow-diffuse {
    box-shadow: 
        0 1px 1px rgb(0 0 0 / 0.08), 
        0 2px 2px rgb(0 0 0 / 0.12), 
        0 4px 4px rgb(0 0 0 / 0.16), 
        0 8px 8px rgb(0 0 0 / 0.20);
}

正如上面示例所示,我们可以让阴影颜色透明值随着每一层的增加而减少或增加,以创造更多或更少的扩散阴影。除此之外,也可以以更高的增量增加模糊度,以增加扩散并创造更柔和的,近乎梦幻的效果:

.shadow-dreamy {
    box-shadow: 
        0 1px 2px rgb(0 / 0 0 / 0.07), 
        0 2px 4px rgb(0 / 0 0 / 0.07), 
        0 4px 8px rgb(0 / 0 0 / 0.07), 
        0 8px 16px rgb(0 / 0 0 / 0.07),
        0 16px 32px rgb(0 / 0 0 / 0.07), 
        0 32px 64px rgb(0 / 0 0 / 0.07);
}

这样的多层阴影看上去就像是带有线性变化(或者说类似缓动函数)的阴影。为了简化阴影参数值的设置,同时构建更具真实生活中阴影的效果(也就是所谓平滑阴影效果),@brumm 为大家构建了一个阴影配置工具。在这个工具上,你可以像下面的视频一样操作,配置出更平滑,更友好,更真实,更梦幻的阴影效果:

有关于这方面更详细的介绍,可以阅读@brumm的博文《Smoother & sharper shadows with layered box-shadows》。

除了使用 @brumm阴影配置工具 之外,也可以使用在生产中使用 @Mike Peutz 写的 PostCSS 插件

/* input.css */
box-shadow: 
    inset 0 1px 1px #000, shadow(15), 
    inset 0 -1px 1px #fff;

/* output.css */
box-shadow: 
    inset 0 1px 1px #000, 
    0px 7px 10px -5px rgba(0, 0, 0, 0.25), 
    0px 15px 24px 2px rgba(0, 0, 0, 0.18), 
    0px 6px 29px 5px rgba(0, 0, 0, 0.11), 
    inset 0 -1px 1px #fff;

/* input.css */
box-shadow: shadow(15 #f00);

/* output.css */
box-shadow: 
    0px 7px 10px -5px rgba(255, 0, 0, 0.25), 
    0px 15px 24px 2px rgba(255, 0, 0, 0.18), 
    0px 6px 29px 5px rgba(255, 0, 0, 0.11);

虽然阴影分层能制作较为真实的阴影效果,但也是有较大代价的。那就是对性能的影响,阴影会层越多,渲染引擎在渲染的过程要做的事情就越多,渲染速度就会越慢。

有关于阴影分层对性能影响,稍后再单独阐述。

阴影分层的顺序

我们已经知道了,在 text-shadowbox-shadow 中可以以逗号(,)作为阴影层的分隔。

注意,后面我们要介绍的 filter 中的 drop-shadow() 函数,它也可以使用多个投影,但和 text-shadow 以及 box-shadow 不同的是,他不是以逗号(,)作为分隔符,而是以空格( )作为分隔符。有关于该函数更详细的介绍放到后面 drop-shadow()一节中。

text-shadowbox-shadow 中使用多层阴影时,每个阴影层都是有顺序的。即 阴影堆叠在彼此之上,且按照它们被声明的顺序,最上面的阴影会在最上面。拿box-shadow来举例:

.shadow {
    box-shadow:
        20px 0 0 0 red,
        40px 0 0 0 blue,
        60px 0 0 0 orange;
}

.shadow {
    box-shadow: 
        0 -20px 0 0 orange,
        0 -40px 0 0 blue,
        0 -60px 0 0 red;
}

虽然 text-shadowbox-shadow 属性的阴影分层顺序是相同的,但 drop-shadow() 的工作方式与他们是不同的。其阴影是以指数形式添加的,即 (2^(阴影数) – 1)。比如:

  • 一个阴影 等于 (2^1 – 1),即一个阴影被渲染出来
  • 两人阴影 等于 (2^2 – 1),即三个阴影被渲染出来
  • 三个阴影 等于 (2^3 – 1),即七个阴影被渲染出来

以此类推。其中一个阴影指的是一个drop-shadow(),两个阴影是指用空格分开的两个 drop-shadow(),如下面代码所示:

/* 渲染出一个阴影 (2^1 – 1) */
.one-drop-shadow {
    filter: drop-shadow(10px 10px 0 red) 
}

/* 渲染出三个阴影 (2^2 – 1) */
.two-drop-shadow {
    filter: 
        drop-shadow(10px 10px 0 red) 
        drop-shadow(20px 20px 0 orange)
}

/* 渲染出七个阴影 (2^3 – 1) */
.two-drop-shadow {
    filter: 
        drop-shadow(10px 10px 0 red) 
        drop-shadow(20px 20px 0 orange)
        drop-shadow(30px 30px 0 blue)
}

如果你从未接触过 CSS 的 filter 属性,并不知道drop-shadow() 函数,也不用担心。后面我们会专门和大家聊该属性!

阴影的形状和扩散

box-shadow 有关自己独特之处,相比于text-shadow,它除了有外阴影之外还有内阴影(inset)和对阴影进行扩展(<spread>)。另外,box-shadow也有自己一些细节之处:

  • box-shadow 投出的阴影是外阴影时(未显式设置inset),该阴影就像元素的边界框(border-box)是不透明的。假设阴影的扩展半径(<spread>)是0,它的周长与边界框(border-box)的大小和形状完全相同。阴影只在边框(border-box)外绘制:它被裁剪在元素的边框内(border-box
  • box-shadow 投射的阴影是内阴影时(显式设置inset),该阴影就像元素的内距边框(padding-box)外的所有东西都是不透明的。假设阴影的扩展半径(<spread>)是0,它的周长与内距框(padding-box)的大小和形状完全相同。阴影只在内距框(padding-box)内绘制:它被裁剪在元素的内距框外(padding-box
  • 如果定义了扩展半径(<spread>),那么上面定义的阴影周长将向外扩展(box-shadow是外阴影)或向内收缩(box-shadow是内阴影),方法是将阴影的直角边缘按扩展距离外延(对于内阴影则内延),并且产生的宽度和高度都是0(这里所说的宽度和高度是指阴影的宽度和高度)

比如下面示例:

.box {
    border: 5px solid blue;
    background-color: orange;
    width: 140px;
    aspect-ratio: 1 / 1;
}

.box-with-radius {
    border-radius: 20px;
}

.box-without-radius {
    border-radius: 0;
}

按上面的描述,设置不同的box-shadow

.box1 {
    box-shadow: 10px 10px rgb(0 0 0 / .4); /* 无模糊半径和扩展半径的外阴影 */
}

.box2 {
    box-shadow: inset 10px 10px rgb(0 0 0 / .4); /* 无模糊半径和扩展半径的内阴影 */
}

.box3 {
    box-shadow: 10px 10px 0 10px rgb(0 0 0 / .4); /* 无模糊半径却有扩展半径的外阴影 */
}

.box4 {
    box-shadow: inset 10px 10px 0 10px rgb(0 0 0 / .4); /* 无模糊半径却有扩展半径的内阴影 */
}

就上面示例而言,当box-shadow运用了扩展半径时为了保持盒子的形状,设有box-radius的盒子的阴影半径随着变化,外阴影的圆角半径会增加,内阴影的半径会减少,它们的增加(或减少)都会基于阴影的扩展半径来计算。然而,为了在border-radius较小的时候创造一个更尖锐的圆角(从而确保圆角和尖角之间的连续性),当边框半径(border-radius)小于阴影扩展半径(box-shadow)或者在内阴影的情况下,小于负值扩展半径的绝对值时,在计算扩展阴影圆角半径时,可以按下面的公式计算:

上面示例中圆角半径(border-radius)是 20px,阴影(box-shadow)扩展半径是10px,按照上面公式,可以计算出它的比例 r = 20 / 10 = 2。即:

20 + 10 * (1 + Math.pow((20 / 10 - 1), 3)) = 40

即,扩展阴影的半径是 40px

注意,上面的计算只适用于无模糊半径的box-shadow的时候。如果box-shadow显式设置模糊半径为非0情况下,则表示所产生的阴影会被模糊化。那么带有阴影的圆角半径也会产生变化,而且在规范中并没有定义确切的算法。

图片和盒子阴影

在 Web 中,图片是一个不可或缺的媒体元素,我们可以通过不同的方式为 Web 页面添加图片,比如:

  • 使用 <img><picture>
  • 使用 background-image

有关于这方面更多的介绍可以阅读:

很多时候也会在图片上使用box-shadow来制作一些效果,比如“晕影”(Vignette)和 图像遮罩(Image Color Screen)等:

  • 晕影:一种摄影技术,图片的边缘柔和地淡化到背景中,这有助于突出图片的主题
  • 图像遮罩:指的是在图片上添加一层淡颜色层(比如带有一定透明度的纯颜色),这样做可以让放在图片上的文本有一定的对比度,提高可阅读性,也可以在一组不相关的图片中创造视觉一致性

但是,CSS 的 box-shadow 直接运用于 <img> 上时,并不能处处如你所愿,比如内阴影直接运用于 <img> 上时:

img {
    box-shadow:inset 5px 5px 5px 5px #09fa00;
}

如下面示例所示,当你选中inset选项时,内阴影并不会运用于<img>元素上:

造成现象的主要原因是 它被认为是一个“空”元素,而不是容器元素

注意,HTML中除了<img>元素是一个可替换元素(Replaced Element)之外,还有 <iframe><video><embed>等,除此之外,有些元素在特定情况下也会被视为可替换元素,比如 <option><audio><canvas><object><applet>等。另外,HTML规范也说了,<input>元素可替换,因为 image 类型的 <input> 元素就像 <img> 一样被替换。但是其他形式的控制元素,包括其他类型的 <input> 元素,被明确地列为非可替换元素(non-replaced elements)。

虽然可替换元素有很多个,但box-shadow的内阴影(inset)用于这些可替换元素时会被视为空元素的只有 <img><video><iframe><embed>。比如,你在 <video> 上使用 box-shadow 内阴影时,它的表现和 <img> 一样。

要避开这个现象,我们要以在<img><video>外套上一个容器元素,比如 div,并且在该元素上使用内阴影:

.media--wrapper {
    box-shadow: inset 0 0 4vmin 3vmin rgb(0 0 0 / .5);
}

img, video {
    position: relative;
    z-index: -1;
}

尝试着调整内阴影的颜色,你可以看到像下面录屏的效果:

但这还不是最佳的解决方案,对于图片而言,把图片运用于background-image才是更好的方案,但对于<video>,上面的解决方案已是较佳的解决方案了。

对于使用 box-shadow 来给图片添加遮罩效果这里就不演示了。我个人认为这不是一个较佳的方案,对于在图片或视频上添加一个颜色透明层,让图片或视频上文本更具阅读性,更好的解决方案是借助 CSS 的伪元素,比如 ::before::after。有关于这方面更详细的介绍,可以阅读:

不规则阴影:drop-shadow

在实际开发过程中,除了要给盒子(CSS中每个元素盒子都上矩形盒子)添加阴影效果之外,也有需要给不规则的图形添加阴影,比如像下面这样的图片:

如果依旧使用 box-shadow 给其添加阴影的话,其阴影效果并不能紧挨着图形的边缘来添加:

.box-shadow {
    box-shadow: 5px 5px 5vmin 5vmin rgb(250 20 20 / 0.5);
}

实际上,我们需要的阴影的效果是像下图这样:

简单地说,box-shadow 并无法实现上图的效果。不过,使用 CSS 的 filter 中的 drop-shadow() 函数要以实现:

filter: drop-shadow()
drop-shadow() = drop-shadow( <color>? && <length>{2,3} )

就语法规则来说,它和 box-shadow非常相似:

drop-shadow()box-shadow还是有本质区别的。drop-shadow()投影实际上是输入图像的Alpha蒙板的一个模糊的、偏移的版本,用特定的颜色绘制并合成在图像下面。简单地说,box-shadow在元素的整个框后面创建一个矩形阴影,而drop-shadow()过滤器则是创建一个符合图像本身形状(Alpha通道)的阴影。

drop-shadow()函数的每个参数和box-shadow基本相似,但有一点需要特别的注意,那就是阴影扩展半径(<spread>)。当阴影扩展半径的值为正值时会导致阴影扩大和变大,反则之会导致阴影缩小。如果未指定,则默认为0,阴影的大小将与输入的图像相同。还有一点就是,在大多数浏览器中还不支持这个参数,效果不会呈现(截止到2022年1月10日,还没有主流浏览器支持<spread>参数)。因此,在使用drop-shadow()时,还不能添加扩展半径,否则将会无效。

还有一点和 box-shadow 不同的是,它也没有内阴影,即 insetdrop-shadow() 函数中是不存在的。

下面这个是 drop-shadow的使用示例:

调整示例中的 drop-shadow() 参数,你将看到每个参数对阴影的影响:

CSS 不同类型阴影的选择

事实上,在 Web 上的任何东西都可以有阴影,开发者有多种方式(多种CSS属性和函数)来创建阴影。但选择正确的阴影类型是有效使用阴影的关键。

我们可以按下面的方式来对阴影类型的使用做出较好的选择:

  • text-shadow :专门为文本创建阴影
  • box-shadow :可以创建符合元素边界框的阴影(矩形框阴影)
  • drop-shadow() :它不是CSS的属性,是一个 CSS 函数,只是 filter 属性中的一个值。它和box-shadow的不同之处在于,它遵循任何元素(包括伪元素)的渲染形状(可以是任何规则形状)

如果你在 Web 中使用 SVG 的话,还可以使用 <feDropShadow> 给 SVG 元素创建阴影,但这不是我们今天要讨论的范畴。

如果你掌握了不同类型的阴影和每一种阴影的独特创造能力,阴影效果的可能性就会变得无限大。从简单的给元素投影,到给元素添加内阴影等,甚至还可以创建有趣的视觉效果,为 UI 添加额外的意义或价值。

阴影中你可能不知道的一些事

正如前面所述,CSS实现阴影有多种方式,不同的方式运用于不同的场景并且能得到不同的效果。但在创建阴影的时候有些细节可能你容易忽略。接下来我们一起来聊聊这些点。

阴影碰到overflow

稍微了解 CSS 的同学都知道,当内容溢出的盒子的时候,使用 overflowvisible 值可以让内容不溢出盒子,溢出的内容可能被截剪、被隐藏,也有可能会出现滚动条。而阴影从视觉上来看是会在盒子外面(除内阴影),换句话说,当阴影碰到overflowhidden的时候,它的表现形式会是什么呢?

在回答这个问题之前,我们可以明确知道的是盒子阴影(box-shadow)或者说drop-shadow() 函数构建的投影是不会参与 CSS 盒子尺寸计算的。只不过,阴影只是视觉上在盒子的外面。当带有盒子阴影的元素显式设置了overflowhidden的时候,阴影并不会被截取。需要注意的是,如果显式设置阴影元素的父容器要是显式设置了overflowhidden,阴影则会被截取。比如下面这个示例:

上面这个示例中,左侧是box-shadow运用于在图片的容器上,右侧是box-shadow直接运用于图片上,当图片容器的overflowhidden时,运用于图片上的阴影会被截取。如下所示:

同样的,drop-shadow()产生的阴影超过overflow的容器时,阴影也会被截取:

如果你想更深入的了解 CSS 的 overflow ,可以移步阅读《你所不知道的CSS Overflow Module》一文。

阴影碰到Clipping 和 Masking

从视觉呈现的角度来看,除了 overflow属性为hidden时要以将溢出的内容被裁剪之外,还可以使用 clip-pathmask。常常使用用clip-pathmask属性来“剪辑”或屏蔽一个元素(视觉上有点类似于overflow:hidden效果)。如果我们在使用了clip-pathmask的元素上使用box-shadow创建的外阴影,则阴影效果会不可见。

:root {
    --x-offset: 20px;
    --y-offset: 20px;
    --blur-radius: 5vw;
    --spread-radius: 5vw;
    --color: #09fa00;
}

.box-shadow {
    box-shadow: var(--x-offset) var(--y-offset) var(--blur-radius) var(--spread-radius) var(--color);
}

.drop-shadow {
    --blur-radius: 5vh;
    --x-offset: 5vw;
    --y-offset: 5vw;
    --color: #f36;
    filter: drop-shadow(
        var(--x-offset) var(--y-offset) var(--blur-radius) var(--color)
    );
}

.clip-path {
    clip-path: inset(0 0 0 0);
}

.mask {
    mask-image: linear-gradient(to bottom, #000, #000);
    mask-size: cover;
}

不过,在使用clip-path构建的不规则图形中使用drop-shadow()构建的投影时,可以把drop-shadow()用于剪切元素的父容器上,则投影不会被裁剪:

.drop-shadow {
    --blur-radius: 10px;
    --x-offset: 15px;
    --y-offset: 15px;
    --color: rgb(250 250 22 / 0.75);
    filter: drop-shadow(
        var(--x-offset) var(--y-offset) var(--blur-radius) var(--color)
    );
}

.clip-path {
    width: 100%;
    height: 100%;
    background: conic-gradient(yellow, lime, blue, violet, red, yellow);
    --path: polygon(50% 0%, 0% 100%, 100% 100%);
    clip-path: var(--path);
}

对于mask也是类似的,把drop-shadow()用于mask的父元素上:

:root {
    --x-offset: 10px;
    --y-offset: 10px;
    --blur-radius: 10px;
    --color: #f36;
    --shadow: var(--x-offset) var(--y-offset) var(--blur-radius) var(--color);
}

.mask-parent {
    filter: drop-shadow(var(--shadow));
}

.mask {
    width: 50vh;
    aspect-ratio: 4 / 3;
    background: #09f;
    mask-image: radial-gradient(circle, #000 50%, transparent 50%);
    mask-size: cover;
    mask-position: center;
}

如果你从未接触过 CSS 的 Clipping 和 Masking 方面的知识,建议你花点时间阅读下面几篇文章:

从上面示例我们不难发现,虽然在剪切元素的父元素上使用 box-shadow 可以显示阴影,但这个阴影是一个盒子阴影,效果不佳,更合理的方式是使用 drop-shadow 来设置剪切元素(特别是运用了clip-pathmask属性的元素)。

分组元素上的阴影

Web 布局的时候,有时候会有元素重叠的布局需求。如果我们给整个重叠在一起的元素组上设置阴影的话,使用box-shadow 添加盒子阴影,就会留下空位(看上去阴影被缺失),如下图所示:

如果我们给每个元素单独添加一个box-shadow,那么每个元素都会有自己的盒子阴影,这可能更不是我们想要的效果。

面对这样的场景,我们使用 drop-shadow() 可以完美的解决这个问题:

上面示例中,左侧的卡片是采用 box-shadow创建的阴影,右侧的卡片使用的是drop-shadow()创建的阴影,相比这下,效果哪个更好是不是立马可见。

阴影和可访问性

WCAG 规范中对文本的可访问性是有明确要求的

上图来自 Spark Design System 的《Color Accessibility》一文。

让我们回头看看 W3C 在 WCAG 2.0 规范中是怎么说的:

设计者可以将字母(文本)后面的背景变暗,或者在字母周围添加一个细细的(thin)的黑色轮廓(至少一个像素的粗细),以保证文本和背景之间的对比度大于 4.5:1

我们曾在《处理图片上文字效果的几种姿势》一文中介绍了几种不同的方式给来增加图像上文本的可识别度。对于给文本添加细的轮廓,除了使用 CSS 的描边属性 text-stroke 之外,CSS 的 text-shadow 是一个较好的解决方案。也就是说,text-shadow 可以帮助提高可访问性,即使用text-shadow来增加文本和图像(或背景)之间的对比度,至少达到 WCAG 的标准的要求。

改变文本和背景之间对比度,只是提高Web可访问性方面的其中一小部分,Web 可访问性(A11Y)的研发也是 Web 的重要体系之一,如果你想更深入的了解和学习这方面的知识,可以阅读小站上提供的 A11Y 的系列教程

阴影和性能

虽然阴影能让视觉效果变得更厚重,更具有质感,但他们对于 Web 的性能来说是致命的。就拿 drop-shadow() 来说吧,虽然能开启浏览器的硬件加速,会创建一个新的合成器层(开启GUP渲染),但你也可能不希望这样的独立合成器层过多,因为它会占用有限的 GPU 内存,并最终会降低 Web 的渲染性能。

不管哪种阴影类型,它们都有可能会设置模糊半径(阴影的模糊度),而这个模糊度又是一个昂贵的消费。当你给阴影添加了模糊半径时,相当于某样东西会变得模糊(阴影层),这个模糊层混合了输出像素周围的像素的颜色,产生一个模糊的结果。比如,你阴影的模糊半径为 2px,那么过滤器需要在每个像素周围的每个方向上查看两个像素来生成混合颜色。这发生在每个输出像素上,也意味着浏览器渲染引擎需要大量的计算,这个计算可能会呈指数级别增长。所以,模糊半径的值越大,需要的计算量就越大,这也就是具有大的模糊半径的阴影要比具有小的模糊半径阴影渲染慢的原因之一。为此,在使用阴影的时候,应该尽可能的少用模糊半径,即使要用,该值也应该尽可能的小。

除了使用小的模糊半径之外,我们还应该尽可能的避免使用多个阴影值(有些同学喜欢使用多个阴影来构建立体效果)。因为,值越多,浏览器渲染引擎需要的计算量也就越多,渲染就会越慢。

还有,我们要尽可能的在动画中避免改变阴影的值,特别是少用box-shadow。可以考虑在动画中使用filterdrop-shadow(),用它来做动画更有表现力,渲染性能也相对会更好一点。注意,这也只是一个要相对值。

如果不希望阴影影响Web的性能,但又不可避免使用阴影,特别是多个阴影效果时,或者在transitionanimation 中改变阴影来提高交互的可识别性。那么我们可以使用一个黑魔法,就是使用伪元素::before::after来构建阴影层,并对其opacity进行动画处理。有关于这方面详细的介绍,可以阅读@tobiasahlin的《How to animate box-shadow with silky smooth performance》一文。下面这个示例,展示了不同类型阴影动效之间的性能差异:

阴影的案例

Web中阴影无处不在的。我们来看几个阴影使用和产生的效果。

这两年在设计圈老有人提 Glassmorphism UINeumorphism UI 设计效果。这两种UI设计中都是重度使用box-shadow的。比如下面这个 Glassmorphism UI:

再来看一个 Neumorphism 的音乐播放器 UI 效果:

前段时间看到 @Michal Malewicz 提出 Claymorphism UI 效果,也是重度使用阴影的 UI效果:

来看一个 Claymorphism 的示例:

使用多阴影,还可以写一些光晕效果,比如下面这个:

历年来 Codepen上的优秀案例都有阴影的影子,比如今年的排名在第72的,就是一个box-shadow写的3D UI效果:

@Krishna Gupta 在 Codepen 上写了个发光的效果,也是box-shadow

@chokcoco 在他的博文《妙用 drop-shadow 实现线条光影效果》详细介绍了使用 drop-shadow创建的光影动效:

再看一个drop-shadow和SVG结合在一起的光影动效:

是不是很有意思呀!使用 text-shadowbox-shadowdrop-shadow() 创建UI和动效的效果还有很多。这里不一一列举了。

小结

如果只是掌握 CSS 阴影相关的属性(text-shadowbox-shadow)或函数(drop-shadow())的基本使用,其实很简单。但要真正的把阴影用好或者说设计出较好的阴影效果,那涉及的东西就多了。比如文章中开头提到的光和阴影相关的知识,因为光源直接影响了阴影的效果。除此之外,阴影还与Web的可访问性以及性能都相关联,而且它们之间让你选择的话,是令人为难的。阴影能增加 UI 的质感和厚重感,但对性能是有较大影响的,为此,很多时候不能鱼和熊掌兼得。最后特别要提出的是,如果在动效中使用阴影,应该尽可能的借助伪元素来完成,这样除了性能更好之外,动效也更细腻。

最后,希望这篇文章能帮助你更好的了解 Web 中的阴影以及 CSS 中不同类型的阴影使用。