background-clip
是我多年来一直了解但很少使用的属性之一。也许只是在 Stack Overflow 问题的解决方案中使用过几次。直到去年,我开始创建我庞大的滑块集合。我选择重现的一些设计稍微复杂一些,并且每个滑块只有一个元素可用,恰好是一个input
元素,这意味着我甚至无法在其上使用伪元素。即使这在某些浏览器中确实有效,它有效的事实实际上是一个错误,我不想依赖它。所有这一切都意味着我最终大量使用了背景、边框和阴影。我也从中学到了很多东西,本文分享了一些经验教训。
在其他任何事情之前,让我们看看background-clip
是什么以及它做什么。
在下图中,我们有一个元素的盒子模型。

如果padding
为0
,则padding-box
的大小与content-box
完全相同,并且内容限制与填充限制重合。

padding: 0
时如果border-width
为0
,则border-box
的大小与padding-box
相同,并且边框限制与填充限制重合。

border-width: 0
时如果padding
和border-width
都为0
,则所有三个盒子(content-box
、padding-box
和border-box
)的大小相同,并且内容限制、填充限制和边框限制全部重合。

padding: 0
且border-width: 0
时默认情况下,背景覆盖整个border-box
(也应用于边框下方),但它们的background-position
(以及基于%
的background-size
)相对于padding-box
。
为了更好地理解这一点,让我们考虑一个例子。我们取一个具有随机尺寸的盒子,给它一个简单的background-size: 50% 50%
渐变背景和一个散列的border
(使用border-image
),这样我们仍然可以通过散列看到边框下方的内容
查看 CSS-Tricks 的 CodePen 上的示例 默认 background-origin 和 background-clip(基本版本) (@css-tricks) 。
在此演示中,我们可以看到渐变背景覆盖了整个border-box
(它在散列边框下方可见)。我们没有指定background-position
,因此它采用默认值 - 0 0
。我们可以看到它相对于padding-box
,因为它从该盒子的左上角(0 0
)开始。我们还可以看到%
中设置的background-size
相对于padding-box
。

border-box
,但从padding-box
的左上角开始在为渐变设置background-size
时(但不是实际图像),我们通常需要两个值才能在浏览器之间获得一致的结果。如果我们只使用一个值,Firefox 会将第二个值视为 100%(根据规范),而其他所有浏览器错误地将第二个值视为等于第一个值。缺少的background-size
值被视为auto
,并且由于渐变没有固有的尺寸或固有的比例,因此无法从这些值中解析auto
值,因此应将其视为100%
。因此,除非我们希望background-size
的两个维度都为100%
,否则我们应该使用两个值。

background-size
不会在浏览器之间产生一致的结果(实时测试);左:Firefox(根据规范,将第二个值视为100%
);右:Chrome/Opera、Safari、IE/Edge(错误地将第二个值视为等于第一个值)我们可以使用background-clip
使背景仅覆盖padding-box
或仅覆盖content-box
。裁剪意味着剪切并不会显示超出裁剪区域的内容,其中裁剪区域是下图中虚线内的区域。

在background-clip: border-box
的默认情况下,裁剪区域是border-box
,因此我们也有边框下方的背景。

background-clip: border-box
如果我们设置background-clip: padding-box
,则裁剪区域是padding-box
,这意味着背景仅在padding-box
限制内显示(它不会出现在边框下方)。

background-clip: padding-box
最后,如果我们有background-clip: content-box
,则裁剪区域是content-box
,因此背景仅在content-box
限制内显示。

background-clip: content-box
以下实时演示说明了这三种情况
查看 CSS-Tricks 的 CodePen 上的示例 背景 - 辅助演示 #2 (@css-tricks) 。
我们还有一个名为background-origin
的属性,它指定background-position
(以及background-size
,如果以%
表示)相对于这三个盒子中的哪一个。
让我们假设我们有一个像之前一样的元素,它有一个散列的border
,并且这次有一个可见的padding
。我们在背景上叠加了一个实际图像和一个渐变。两者都具有background-size: 50% 50%
并且不重复。此外,该图像具有background-position: 100% 100%
(我们将渐变保留为默认的0 0
)
background: linear-gradient(to right bottom,
#e18728, #be4c39, #9351a6, #4472b9),
url(tiger_lily.jpg) 100% 100%;
background-repeat: no-repeat;
background-size: 50% 50%;
以下演示说明了对于background-origin
的三个可能值(border-box
、padding-box
和content-box
)会发生什么情况
查看 CSS-Tricks 的 CodePen 上的示例 背景 - 辅助演示 #3 (@css-tricks) 。
background-position
的实际图像指定的100% 100%
是background-origin
指定的盒子的100% 100%
。同时,background-size
指定的50% 50%
表示background-origin
指定的盒子的宽度和高度的一半。
在background
简写中,可以在图层的末尾按此顺序指定background-origin
和background-clip
。由于它们都采用盒子值,如果只指定了一个盒子值,则两者都设置为该值。如果指定了两个盒子值,则background-origin
设置为第一个,background-clip
设置为第二个。如果未指定盒子值,则它们只采用默认值(background-origin
为padding-box
,background-clip
为border-box
)。
好了!现在让我们看看如何利用它来发挥我们的优势!
边框和背景之间的透明间隙
有些人可能记得我们可以使用background-clip
获得半透明边框。但是,我们也可以在边框和具有背景的区域之间引入空间,而无需引入额外的元素。最简单的方法是在border
之外添加一个padding
,并将background-clip
设置为content-box
。通过使用简写并只使用一个盒子值,我们也正在将background-origin
设置为content-box
,但在这种情况下是可以的,它没有任何不希望的效果。
border: solid .5em #be4c39;
padding: .5em;
background: #e18728 content-box;
将背景裁剪到content-box
意味着它不会扩展到内容限制之外。除此之外,我们没有背景,所以我们可以看到元素下方的内容。添加border
意味着我们在填充限制和边框限制之间看到该border
。但是,如果padding
不为零,我们仍然在内容限制和填充限制之间有一个透明区域。

我们可以使用此 CodePen 进行实时测试
查看 Ana Tudor 的 CodePen 上的示例 边框和背景之间的空间 (@thebabydino) 。
我们可以通过添加一个drop-shadow()
过滤器使事情变得更有趣,这会使整个事物发出黄色的光芒
查看 Ana Tudor 的 CodePen 上的示例 边框和背景之间的空间 v2 (@thebabydino) 。
前缀提醒:我看到很多资源为 CSS 过滤器添加了-moz-
和-ms-
前缀。请不要这样做!自从 CSS 过滤器首次实现(Firefox 34,2014 年秋季)以来,它们在 Firefox 中一直没有前缀,现在它们已在 Edge 中(在某个标志后面)实现 - 也未添加前缀!因此,CSS 过滤器从未需要-moz-
或-ms-
前缀,添加它们完全没有用,它们唯一的作用是增加样式表的体积。
如果我们对background-image
和border-image
都使用渐变,我们还可以获得一个看起来很酷的效果。我们将创建一个从顶部开始的纯橙色/红色的渐变,然后逐渐淡化为完全透明。由于只有阴影不同,否则使用的渐变相同,因此我们创建了一个 Sass 函数。
@function fade($c) {
return linear-gradient($c, rgba($c, 0) 80%);
}
div {
border: solid 0.125em;
border-image: fade(#be4c39) 1;
padding: 0.125em;
background: fade(#e18728) content-box;
}
我们可以使用此 CodePen 进行实时测试
查看 Ana Tudor 的 CodePen 上的示例 边框和背景之间的空间 v3 (@thebabydino) 。
除非我们中间只有简短的文本,否则使用填充在背景和边框之间创建空间的方法并不是最好的方法。如果我们有更多的文本……好吧,它看起来很糟糕。
查看 Ana Tudor 的 CodePen 上的示例 边框和背景之间的空间 v4 (@thebabydino) 。
问题在于文本从橙色background
的边缘开始,我们无法添加padding
,因为我们已经将其用于透明间隙。我们可以添加一个额外的元素……或者我们可以使用box-shadow
!
box-shadow
可以接受 2
、3
或 4
个长度值。第一个是 **x
偏移量**(决定阴影向右移动多少),第二个是 **y
偏移量**(决定阴影向下移动多少),第三个是 **模糊半径**(决定阴影边缘的模糊程度),第四个是 **扩展半径**(决定阴影向各个方向扩展的程度)。
下面的 **交互式演示** 允许您玩弄这些值——点击其中任何一个,就会弹出一个带有滑块的窗口。
查看 CodePen 上 CSS-Tricks (@css-tricks) 的作品 how `box-shadow` works v2。
请注意,虽然模糊半径必须始终大于或等于零,但偏移量和扩展半径可以为负数。负偏移量只是将阴影移动到其轴的负方向(左或上)。负扩展半径表示阴影收缩而不是扩展。
这里需要注意的另一件重要的事情——因为它对我们的用例很方便——是 box-shadow
永远不会显示在 border-box
占据的空间下方,即使该空间是(半)透明的。
如果我们将偏移量和模糊半径保持为零,但将扩展半径设置为正值,那么我们得到的效果看起来像一个在所有方向上宽度相同的第二个实心边框,从实际边框的限制开始向外延伸。

box-shadow
模拟边框请注意,这种边框不一定要在所有方向上具有相同的宽度。我们可以通过调整偏移量和扩展半径来赋予它不同的边框宽度,前提是水平边框宽度的总和等于垂直边框宽度的总和。如果我们不需要边框是半透明的,使用多个阴影可以帮助我们克服此限制,但这将是一种使事情复杂化而不是简化的事情的解决方案。
回到我们的演示,我们使用 box-shadow
的扩展来模拟边框,使用实际的 border
创建透明间隙,将 background-clip
设置为 padding-box
,并让 padding
发挥作用。
border: solid 1em transparent;
padding: 1em;
box-shadow: 0 0 0 1em #be4c39;
background: #e18728 padding-box;

它可以在以下 CodePen 中看到实际效果
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 space between border and background v5。
我们还可以通过模拟 inset
阴影来模拟边框。在这种情况下,它从填充限制(padding
和 border
区域之间的限制)开始,并向内延伸扩展指定的距离。

inset
box-shadow
模拟第二个边框因为我们可以有多个盒阴影,所以我们可以用这种方式模拟多个边框。让我们考虑一下我们有两个边框的情况,其中一个为 inset
边框。如果元素的实际 border
不为零且为 transparent
,并且 background-clip
设置为 padding-box
,那么我们会在其内部和外部组件之间获得一个带有透明区域(实际边框区域)的伪双边框。请注意,这还需要增加 padding
以补偿 inset
box-shadow
占据的空间。
border: solid 1em transparent;
padding: 2em; // increased from 1em to 2em to compensate for inner "border"
box-shadow:
0 0 0 0.25em #be4c39 /* outer "border" */,
inset 0 0 0 1em #be4c39 /* inner "border" */;
background: #e18728 padding-box;
它可以在此 CodePen 中进行实时测试,我们还使用 drop-shadow()
过滤器来实现发光效果。
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 space between border and background v6。
单个元素(无伪类)目标,具有平滑边缘
假设我们想要获得如下所示的目标,并且限制条件是我们只能使用一个元素且不能使用伪元素。
第一个想法是使用 repeating-radial-gradient
。我们的目标结构大致如下所示
因此,目标的一半是 9
个单位,这意味着目标的水平和垂直尺寸均为 18
个单位。我们的重复径向渐变在第一个单位为黑色,然后到第三个单位透明,然后再次为黑色,然后再次透明……这听起来像一个重复。除了我们从 0
到 1
只有一个单位,第一次我们有一个黑色区域,但第二次我们有一个黑色区域,它从 3
到 5
——那是两个单位!所以……我们不应该从 0
开始,而应该从 -1
开始,对吧?嗯,这应该可以工作,根据规范。
$unit: 1em;
background: repeating-radial-gradient(
#000 (-$unit), #000 $unit,
transparent $unit, transparent 3*$unit
);
此 CodePen 演示了这个想法。
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 repeating-radial-gradient for target。
这里第一个问题是 IE 对此的工作方式有不同的看法。

幸运的是,这 在 Edge 中已修复,但如果需要 IE 支持,它仍然是一个问题。我们可以通过使用普通径向渐变来解决这个问题,因为我们不需要那么多圆圈。代码更多,但也不是那么糟糕……
background: radial-gradient(
#000 $unit, transparent 0,
transparent 3*$unit, #000 0,
#000 5*$unit, transparent 0,
transparent 7*$unit, #000 0,
#000 9*$unit, transparent 0
);
我们可以在此 CodePen 中看到它在实际中的效果
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 radial-gradient for target。
现在圆圈在所有浏览器中的分布方式相同,但我们仍然存在另一个问题:在 IE/Edge 中,边缘可能不像原始图像那样平滑,但在 Firefox 和 Chrome 中看起来很丑!

我们可以使用 非锐利过渡技巧
background: radial-gradient(
#000 calc(#{$unit} - 1px),
transparent $unit,
transparent calc(#{3*$unit} - 1px),
#000 3*$unit,
#000 calc(#{5*$unit} - 1px),
transparent 5*$unit,
transparent calc(#{7*$unit} - 1px),
#000 7*$unit,
#000 calc(#{9*$unit} - 1px),
transparent 9*$unit
);
实时测试
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 radial-gradient for target v2。
嗯,这改进了 IE(结果已经看起来不错)和 Firefox 中的情况,但 Chrome 中的边缘仍然看起来很丑。

也许径向渐变毕竟不是最好的解决方案。如果我们将上一节中 background-clip
和 box-shadow
的解决方案应用于此问题会怎样?我们可以使用外部 box-shadow
来实现外圆,并使用 inset
阴影来实现内圆。它们之间的空间由透明边框占用。我们还将 background-clip 设置为 content-box
,并为元素提供足够的填充,以便在中心圆盘和内圆之间有一个透明区域。
border: solid 2*$unit transparent;
padding: 4*$unit;
width: 2*$unit; height: 2*$unit;
border-radius: 50%;
box-shadow:
0 0 0 2*$unit #000,
inset 0 0 0 2*$unit #000;
background: #000 content-box;
我们可以在以下 CodePen 中看到它的工作原理,没有锯齿状边缘,也没有麻烦。
查看 CodePen 上 CSS-Tricks (@css-tricks) 的作品 CSS target with smooth edges。
逼真的控件
我第一次想到这个主意是在尝试为范围输入的滑块、轨道以及非 WebKit 浏览器的进度(填充)设置样式时。浏览器为这些组件提供了伪元素。
对于轨道,我们有 -webkit-slider-runnable-track
、-moz-range-track
和 -ms-track
。对于滑块,我们有 -webkit-slider-thumb
、-moz-range-thumb
和 -ms-thumb
。对于进度/填充,我们有 -moz-range-progress
、-ms-fill-lower
(都在滑块左侧)和 -ms-fill-upper
(都在滑块右侧)。WebKit 浏览器没有提供允许将滑块前后的部分样式设置为不同的伪元素。
这些看起来不一致且难看,但更难看的是我们无法将相同组件的所有浏览器版本一起列出以设置其样式。类似这样的不起作用
input[type='range']::-webkit-slider-thumb,
input[type='range']::-moz-range-thumb,
input[type='range']::-ms-thumb { /* styles here */ }
我们必须始终像这样编写它们
input[type='range']::-webkit-slider-thumb { /* styles here */ }
input[type='range']::-moz-range-thumb { /* styles here */ }
input[type='range']::-ms-thumb { /* styles here */ }
这看起来像是一种非常 WET 的代码编写风格,在很多情况下确实如此——尽管考虑到滑块在各个浏览器中的许多不一致性,它在跨领域进行平衡方面也很有用。我对此的解决方案是使用 thumb()
混合器,并可能为其提供处理不一致性的参数。例如,像这样
@mixin thumb($flag: false) {
/* styles */
@if $flag { /* more styles */ }
}
input[type='range'] {
&::-webkit-slider-thumb { @include thumb(true); }
&::-moz-range-thumb { @include thumb(); }
&::-ms-thumb { @include thumb(); }
}
但让我们回到如何设置样式的问题。我们只能为 Chrome/Opera 中的这些组件添加伪元素,这意味着在重现其外观时,我们必须尽可能接近它,而无需依赖伪元素。这使得我们只能在滑块元素本身使用背景、边框、阴影、过滤器。
让我们看几个例子!
软塑料控件
举个视觉例子,想想下面滑块的滑块。

首先想到的是,它就像一个渐变 background
和一个渐变的 border-image
一样简单,然后在那里添加一个 box-shadow
,这样一个控件就完成了。
border: solid 0.375em;
border-image: linear-gradient(#fdfdfd, #c4c4c4) 1;
box-shadow: 0 0.375em 0.5em -0.125em #808080;
background: linear-gradient(#c5c5c5, #efefef);
这确实有效(使用 button
元素代替滑块滑块以简化操作)
查看 CodePen 上 Ana Tudor (@thebabydino) 的作品 soft plastic button (square)。
除了我们的控件是圆形的,而不是方形的。我们只需要设置border-radius: 50%
就可以了,对吧?嗯……这不起作用,因为我们使用了border-image
,它导致元素本身忽略了border-radius
,不过有趣的是,如果存在box-shadow
,它仍然会应用于box-shadow
。
那我们该怎么办呢?使用background-clip
!我们首先给元素设置一个非零的padding
,没有边框,并使用border-radius: 50%
使其成为圆形。然后,我们叠加两个渐变背景,最上面的一个限制在content-box
内(注意剪裁作为背景简写的一部分应用)。最后,我们添加两个盒子阴影,第一个是深色阴影,用于创建控件下方的阴影,第二个是inset
阴影,用于稍微加深控件外部部分的底部和侧面。
border: none; /* makes border-box ≡ padding-box */
padding: .375em;
border-radius: 50%;
box-shadow: 0 .375em .5em -.125em #808080,
inset 0 -.25em .5em -.125em #bbb;
background:
linear-gradient(#c5c5c5, #efefef) content-box,
linear-gradient(#fdfdfd, #c4c4c4);
最终结果可以在这个Pen中看到
查看Pen 软塑料按钮(真正的圆形!),作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
磨砂控件
类似于以下图像中的滑块的拇指

这看起来与之前的情况非常相似,只是现在顶部有一条浅色线条,中间部分有一个内阴影。如果我们使用外部box-shadow
来创建浅色线条,那么整个东西就不会再是圆形的了,除非我们也降低它的高度来补偿阴影。这意味着我们需要进行更多计算来确定内部部分的位置。如果我们改为使用inset
阴影,那么我们就无法将其用于内部部分的深色阴影。但是,我们可以使用一个radial-gradient
来模拟它,该渐变可以方便地调整大小、定位和剪裁到content-box
。这意味着与之前的情况相同的策略,只是我们在其他背景的顶部叠加了一个额外的radial-gradient
。径向渐变的实际background-size
大于content-box
,因此我们可以将其向下移动,而不会将其上边缘带入内容限制内。
border: none; /* makes border-box ≡ padding-box */
padding: .625em;
width: 1.75em; height: 1.75em;
border-radius: 50%;
box-shadow:
0 1px .125em #444 /* dark lower shadow */,
inset 0 1px .125em #fff /* light top hint */;
background:
/* inner shadow effect */
radial-gradient(transparent 35%, #444)
50% calc(50% + .125em) content-box,
/* inner background */
linear-gradient(#bbb, #bbb) content-box,
/* outer background */
linear-gradient(#d0d3d5, #d2d5d7);
background-size:
175% 175% /* make radial-gradient bg larger */,
100% 100%, 100% 100%;
我们可以在这个Pen中看到它
查看Pen 磨砂按钮,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
3D 控件
例如,以下滑块的拇指

这个稍微复杂一些,需要三个盒子(content-box
、padding-box
和 border-box
)都不同,以便我们可以叠加背景并使用background-clip
获得所需的效果。
因此,对于滑块的主体部分,我们在另一个剪裁到padding-box
的渐变background
的顶部叠加了一个剪裁到content-box
的渐变background
,这两个渐变都覆盖在第三个剪裁到border-box
的linear-gradient
上。我们还使用内阴影box-shadow
来突出显示填充限制(padding
和border
之间)。
border: solid .25em transparent;
padding: .25em;
border-radius: 1.375em;
box-shadow:
inset 0 1px 1px rgba(#f7f7f7, .875) /* top */,
inset 0 -1px 1px rgba(#bbb, .75) /* bottom */;
background:
linear-gradient(#9ea1a6, #fdfdfe) content-box,
linear-gradient(#fff, #9c9fa4) padding-box,
linear-gradient(#eee, #a4a7ab) border-box;
这部分的结果可以在这个Pen中看到
查看Pen 3D 按钮,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
现在剩下的就是那个小圆形部分。在这种情况下,我感觉确实需要一个伪元素。我最终确实为 Blink 选择了这条路线,但通过在线性渐变的顶部叠加两个径向渐变,设法为其他浏览器获得了外观不错的回退。
查看Pen 3D 按钮 #2,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
我们可能能够通过选择更好的色调、额外的径向渐变甚至使用background-blend-mode
来更接近我们想要的效果,但我没有那种艺术感来做这样的事情。
使用伪元素,获得所需结果要容易得多——我们首先需要正确地定位和调整其大小,使用border-radius: 50%
使其成为圆形。然后,我们为其设置padding
,没有边框,并使用两个渐变作为background
,最上面的一个是在content-box
内剪裁的径向渐变。
padding: .125em;
background:
radial-gradient(circle at 50% 10%,
#f7f8fa, #9a9b9f) content-box,
linear-gradient(#ddd, #bbb);
这方面的结果可以在以下Pen中看到
查看Pen 3D 按钮 #3,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
对于实际的滑块拇指,我在所有地方都使用了相同的拇指background
,并在其顶部叠加了径向渐变,并在径向渐变的正上方添加了 Blink 的伪元素。这是因为 Safari 中的拇指滑块样式是通过::-webkit-slider-thumb
应用的,但 Safari 不支持拇指(或轨道)上的伪元素。因此,如果我要从应用于::-webkit-slider-thumb
的样式中删除回退,那么 Safari 根本不会显示圆形部分。
深度幻觉
以下滑块的轨道说明了这个想法

就像之前一样,我们通过为元素设置一个非零透明的border
、一个padding
,并叠加具有不同background-clip
值的背景来实现这一点(请记住,那些具有content-box
值的背景需要位于那些具有padding-box
值的背景之上,而那些具有padding-box
值的背景需要位于那些具有border-box
值的背景之上)。在这种情况下,我们有一个较浅的linear-gradient
来覆盖border
区域,一个较深的渐变加上几个径向渐变,我们将它们缩小并设置为不重复,以进一步加深两端的颜色,用于padding
区域,以及一个非常深的渐变用于内容区域。然后,我们为其设置一个border-radius
,该值至少为内容区域的height
的一半加上两倍的padding
和两倍的border
。我们还添加了一个内阴影box-shadow
来微妙地突出显示填充限制。
border: solid .375em transparent;
padding: 1em 3em;
width: 15.75em; height: 1.75em;
border-radius: 2.25em;
background:
linear-gradient(#090909, #090909) content-box,
radial-gradient(at 5% 40%, #0b0b0b, transparent 70%)
no-repeat 0 35% padding-box /* left */,
radial-gradient(at 95% 40%, #111, transparent 70%)
no-repeat 100% 35% padding-box /* right */,
linear-gradient(90deg, #3a3a3a, #161616) padding-box,
linear-gradient(90deg, #2b2d2c, #2a2c2b) border-box;
background-size: 100%, 9em 4.5em, 4.5em 4.5em, 100%, 100%;
但这里有一个问题,可以在以下Pen中看到
查看Pen 深度幻觉,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
由于border-radius
的工作方式——内容区域的半径是我们指定的半径减去border-width
减去padding
,最终结果为负数——因此内容区域的角没有圆角。好吧,我们可以解决这个问题!我们可以通过同时使用线性渐变和径向渐变来模拟我们想要的内容区域形状。
我们的做法是首先确保内容区域的width
是其height
的倍数(在我们的例子中,15.75em = 9*1.75em
)。我们首先叠加一个不重复的linear-gradient
,将其定位在中间,垂直覆盖内容区域的整个height
,但在左右两端各留出内容区域height
的一半的空白。在它的上面,我们添加一个radial-gradient
,其background-size
在水平和垂直方向上都等于内容区域的height
。
查看Pen 深度幻觉 #2,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
金属控件
例如,类似于下面所示的按钮

这个稍微复杂一些,让我们分解一下。首先,我们通过设置相等的width
和height
并设置border-radius: 50%
来使按钮呈圆形。然后,我们确保它具有box-sizing: border-box
,以便border-width
和padding
向内缩进(从我们设置的尺寸中减去)。现在,下一个合乎逻辑的步骤是为其设置透明边框和填充。到目前为止,这就是我们拥有的
$d-btn: 27em; /* control diameter */
$bw: 1.5em; /* border-width */
button {
box-sizing: border-box;
border: solid $bw transparent;
padding: 1.5em;
width: $d-btn; height: $d-btn;
border-radius: 50%;
}
它还没有任何外观,这是因为整个外观是通过我们尚未添加的两个属性实现的:box-shadow
和background
。
查看Pen 金属控件 – 步骤 0,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
在执行任何其他操作之前,让我们稍微分解一下控件

从外到内,我们有
- 一个带LED灯的粗外环
- 一个细内环
- 一个穿孔区域(包括以下内部部分周围的青色辉光)
- 一个大的中心部分
粗外环是边框区域,中心部分是内容区域,介于两者之间的所有部分(细内环和穿孔部分)是填充区域。我们使用内阴影box-shadow
创建细内环
box-shadow:
/* discrete dark shadow to act as separator from outer ring */
inset 0 0 1px #666,
/* darker top area */
inset 0 1px .125em #8b8b8b,
inset 0 2px .25em #a4a2a3,
/* darker bottom area */
inset 0 -1px .125em #8b8b8b,
inset 0 -2px .25em #a4a2a3,
/* the base circular strip for the inner ring */
inset 0 0 0 .375em #cdcdcd;
我们还添加了另外两个外部阴影box-shadow
,一个用于外环的顶部浅色高光,另一个用于控件下方的离散深色阴影,所以现在我们有
box-shadow:
0 -1px 1px #eee,
0 2px 2px #1d1d1d,
inset 0 0 1px #666,
inset 0 1px .125em #8b8b8b,
inset 0 2px .25em #a4a2a3,
inset 0 -1px .125em #8b8b8b,
inset 0 -2px .25em #a4a2a3,
inset 0 0 0 .375em #cdcdcd;
仍然不多,但比以前多了
查看Pen 金属控件 – 步骤 1,作者 Ana Tudor (@thebabydino) 在 CodePen 上发布。
现在,我们必须从上到下叠加三种类型的背景:限制在content-box
内(创建中心区域)、限制在padding-box
内(创建穿孔区域和青色辉光)以及限制在border-box
内(创建粗外环和LED灯)。
我们从中心区域开始,这里有一些离散的圆形线条,它们是由三个重复的径向渐变叠加而成,渐变的停止点的值基于蝉原理,并使用圆锥渐变创建圆锥反射。请注意,圆锥渐变目前尚不受任何浏览器支持,因此我们现在需要使用polyfill。
background:
/* ======= content-box ======= */
/* circular lines - 13, 19, 23 being prime numbers */
repeating-radial-gradient(
rgba(#e4e4e4, 0) 0,
rgba(#e4e4e4, 0) 23px,
rgba(#e4e4e4, .05) 25px,
rgba(#e4e4e4, 0) 27px) content-box,
repeating-radial-gradient(
rgba(#a6a6a6, 0) 0,
rgba(#a6a6a6, 0) 13px,
rgba(#a6a6a6, .05) 15px,
rgba(#a6a6a6, 0) 17px) content-box,
repeating-radial-gradient(
rgba(#8b8b8b, 0) 0,
rgba(#8b8b8b, 0) 19px,
rgba(#8b8b8b, .05) 21px,
rgba(#8b8b8b, 0) 23px) content-box,
/* conic reflections */
conic-gradient(/* random variations of some shades of grey */
#cdcdcd, #9d9d9d, #808080,
#bcbcbc, #c4c4c4, #e6e6e6,
#dddddd, #a1a1a1, #7f7f7f,
#8b8b8b, #bfbfbf, #e3e3e3,
#d2d2d2, #a6a6a6, #858585,
#8d8d8d, #c0c0c0, #e5e5e5,
#d6d6d6, #9e9e9e, #828282,
#8f8f8f, #bdbdbd, #e3e3e3, #cdcdcd)
content-box;
现在它终于开始有点样子了!
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 金属控件 - 步骤 2。
我们继续处理穿孔区域。青绿色辉光只是在外部区域径向渐变到透明,而穿孔基于 Lea Verou 五年前整理的图库中的碳纤维图案 - 对我这种艺术细胞欠佳的人来说,它仍然非常有用。
$d-hole: 1.25em; /* perforation diameter*/
$r-hole: .5*$d-hole; /* perforation radius */
background:
/* ======= padding-box ======= */
/* cyan glow */
radial-gradient(
#00d7ff 53%, transparent 65%) padding-box,
/* holes */
radial-gradient(
#272727 20%, transparent 25%)
0 0 / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(
#272727 20%, transparent 25%)
$r-hole $r-hole / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(#444 20%, transparent 28%)
0 .125em / #{$d-hole} #{$d-hole}
padding-box,
radial-gradient(#444 20%, #3d3d3d 28%)
#{$r-hole} #{$r-hole + .125em} / #{$d-hole} #{$d-hole}
padding-box
看起来我们正在接近一个体面的外观。
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 金属控件 - 步骤 3。
基本的粗外环(不含 LED)是用单个圆锥渐变创建的。
conic-gradient(
#b5b5b5, #8d8d8d, #838383,
#ababab, #d7d7d7, #e3e3e3,
#aeaeae, #8f8f8f, #878787,
#acacac, #d7d7d7, #dddddd,
#b8b8b8, #8e8e8e, #848484,
#a6a6a6, #d8d8d8, #e3e3e3,
#8e8e8e, #868686, #a8a8a8,
#d5d5d5, #dedede, #b5b5b5) border-box;
我们现在有一个金属控件了!
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 金属控件 - 步骤 4。
此时它没有 LED,所以让我们修复它!
每个 LED 由两个非重复的径向渐变叠加而成。上面的一个是实际的 LED,下面的一个在垂直方向上稍微偏移,在 LED 的下部创建更亮的亮点。这与穿孔区域中孔的效果基本相同。下面的渐变始终相同,但上面的渐变根据 LED 是否开启而有所不同。
我们将 LED 设为开启到第 $k
个。因此,在我们使用青绿色变体作为顶部渐变的地方,之后我们使用灰色渐变。
我们有 24
个 LED,它们位于穿过其边框区域中间的圆圈上。因此,它的半径是控件的半径减去边框宽度的一半。
我们使用 Sass 生成所有这些渐变。我们首先创建一个空的渐变列表,然后循环,在每次迭代中,我们将两个渐变添加到列表中。计算它们的位置,使它们位于前面提到的圆圈上。第一个渐变取决于循环索引,而第二个渐变始终相同(只是在圆圈上的另一个位置)。
$d-btn: 27em;
$bw: 1.5em;
$r-pos: .5*($d-btn - $bw);
$n-leds: 24;
$ba-led: 360deg/$n-leds;
$d-led: 1em;
$r-led: .5*$d-led;
$k: 7;
$leds: ();
@for $i from 0 to $n-leds {
$a: $i*$ba-led - 90deg;
$x: .5*$d-btn + $r-pos*cos($a) - $r-led;
$y: .5*$d-btn + $r-pos*sin($a) - $r-led;
$leds: $leds,
if($i < $k,
(radial-gradient(circle, #01d6ff,
#178b98 .5*$r-led,
rgba(#01d6ff, .35) .7*$r-led,
rgba(#01d6ff, 0) 1.3*$r-led) no-repeat
#{$x - $r-led} #{$y - $r-led} /
#{2*$d-led} #{2*$d-led} border-box),
(radial-gradient(circle, #898989,
#4d4d4d .5*$r-led, #999 .65*$r-led,
rgba(#999, 0) .7*$r-led) no-repeat
$x $y / #{$d-led} #{$d-led} border-box)
),
radial-gradient(circle,
rgba(#e8e8e8, .5) .5*$r-led,
rgba(#e8e8e8, 0) .7*$r-led) no-repeat
$x ($y + .125em) / #{$d-led} #{$d-led}
border-box;
}
最终结果可以在这个Pen中看到
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 1 个元素(无伪元素)金属控件。
垂直平面上的阴影
考虑控件位于屏幕垂直平面上的情况,我们希望在下方水平平面上有一个阴影。如下面的图片所示

我们希望仅使用一个元素和不使用伪元素来重现此效果。
在这种情况下,使用具有不同 background-clip
和 background-origin
值的背景叠加可以解决问题。我们使用两个背景创建实际按钮,顶部的一个剪裁到 content-box
,下面的一个剪裁到 padding-box
,并使用带有 background-clip
和 background-origin
设置为 border-box
的 radial-gradient()
背景来创建阴影。
基本样式与上一节中的金属控件非常相似。
$l: 6.25em;
$bw: .1*$l;
border: solid $bw transparent;
padding: 3px;
width: $l; height: $l;
border-radius: 1.75*$bw;
我们在周围设置一个较厚的透明边框,以便我们在底部边框区域有足够的空间来重现阴影。我们对所有边框执行此操作,而不仅仅是对底部边框执行此操作,因为我们希望 padding-box
的所有角都具有相同的对称圆角(如果您需要复习一下它是如何工作的,请查看 Lea Verou 关于 border-radius
的精彩演讲)。
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 border-width
& padding-box
角圆角。
最上面的第一个 background
是一个 conic-gradient()
,用于创建圆锥金属反射。它被剪裁到 content-box
。就在它下面,我们有一个简单的 linear-gradient()
剪裁到 padding-box
。我们使用三个内嵌盒子阴影使第二个背景不那么平坦 - 在周围添加另一个阴影,模糊度为零,正向扩展阴影,使用半透明的白色阴影使其顶部变亮,使用半透明的黑色阴影使其底部变暗。
box-shadow:
inset 0 0 0 1px #eedc00,
inset 0 1px 2px rgba(#fff, .5),
inset 0 -1px 2px rgba(#000, .5);
background:
conic-gradient(
#edc800, #e3b600, #f3cf00, #ffe800,
#ffe900, #ffeb00, #ffe000, #ebc500,
#e0b100, #f1cc00, #fcdc00, #ffe500,
#fad900, #eec200, #e7b900, #f7d300,
#ffe800, #ffe300, #f5d100, #e6b900,
#e3b600, #f4d000, #ffe400, #ebc600,
#e3b600, #f6d500, #ffe900, #ffe90a,
#edc800) content-box,
linear-gradient(#f6d600, #f6d600) padding-box
这给了我们金属按钮(还没有阴影)。
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 金属按钮 - 无阴影。
对于阴影,我们叠加第三个背景,为此我们同时将 background-clip
和 background-origin
设置为 border-box
。此背景是一个非重复的 radial-gradient()
,其位置附加到 bottom
(以及水平中间),并且我们将其在垂直方向上缩小,以便它适合该底部边框区域,甚至留出一些空间 - 因此,我们将其设置为 border-width
的 .75
倍左右。
radial-gradient(rgba(#787878, .9), rgba(#787878, 0) 70%)
50% bottom / 80% .75*$bw no-repeat border-box
就是这样!您可以在以下 CodePen 中玩弄这些按钮。
查看 CodePen 上 Ana Tudor (@thebabydino) 编写的 带有阴影的金属按钮(无伪元素)。
background-clip
确实有其用例!尤其是在元素边缘周围叠加多个效果时。
哇![抱歉,Chris……我知道这没什么帮助;但是,它必须要说出来!]
伙计,在你看完所有这些例子之前,我根本想不到任何好的用例。这些例子看起来太棒了!
感谢在这里完成的工作,但是难道整个事情变得如此复杂,使用 SVG 更有意义吗?
是的,拟物化设计难道已经死了吗?
@Kyriakos:有趣的问题。我只是做了其中一个按钮的 SVG 版本,看看感觉如何,我个人觉得它更复杂。我可能不像使用 CSS 那样熟悉 SVG 渐变和滤镜,但我非常确定你无法使用比 CSS 版本更少的代码来创建准确的 SVG 版本。我的代码量是它的两倍多。也许在 SVG 方面更擅长的某个人可以缩短一点,但我怀疑它是否能比 CSS 版本更小。
此外,我不知道如何用 SVG 编写最后两个用例(金属外观按钮)……
@AOK:我无法回答。我不知道拟物化设计是什么意思。我听到人们使用它并向我解释它,但我就是不明白。甚至不确定我是否理解“设计”的含义,尽管我用它来描述“其他人绘制的东西,我已经编写过代码/打算尝试编写代码”。
这取决于用法。
对于样式输入控件,这是一种比 SVG 更好的使用原生元素的方法。
@Ana - 拟物化设计是指设计(通常是用户界面)元素保留在正在模拟的原始事物中必要的视觉线索。例如,您的软塑料控件是拟物化的,因为它通过使用阴影和渐变来模拟现实世界中的软塑料控件。
顺便说一句,它们看起来都很棒,我真的很喜欢阅读这篇文章,并且对您使用 CSS 所做的事情印象深刻!
@AOK - 我不认为拟物化设计已经“死了”。它只是不再是趋势。它将始终拥有自己的位置,并且我认为它永远不会“消失”。
很棒的作品。谢谢,Ana。
真的很喜欢这篇文章。让我思考了一些新的策略。在看您的滑块时,我的键盘上可能也留下了口水。
更喜欢它!
哇!这是一些厉害的 CSS!令人印象深刻的文章!你就像一个 CSS 向导……无论那是什么。
Ana,你太不可思议了。你如何分解复杂的概念并处理计算和几何问题真是太厉害了!我喜欢你大脑的工作方式,你那里有一些疯狂的机器!你的作品令人印象深刻,你总是带来一些新东西。我将收藏这篇文章,并在几天内仔细阅读它。感谢您投入所有这些时间,并与世界分享 :)
非常喜欢“垂直平面上的阴影”部分。让我真正思考可以做多少事情。感谢这篇文章。在尝试重复你的目标演示时,我发现 Chrome 中的一个错误并也报告了它。再次感谢! :)
https://code.google.com/p/chromium/issues/detail?id=586993