阅读 587

2021年你可能不知道的 CSS 特性

作者:大貘

简介

如果您有关注过这两年的 CSS 发展状态报告2019年2020年)的话,不难发现,在报告中有专门关于 CSS 新特性一项的介绍: ​

上图截取 2020 年 CSS 发展状态报告

是不是有种似有相识的感觉,或者有一些又是那么的陌生。对于我而言,这些都不陌生。不过我想说的是上图(或者报告)中提到的只是冰山一角,有很多是没有出现在调查问卷和报告中。比如接下来内容中提到的伪类选择器,内容可见性,容器查询等等。 ​

嗯!先从 CSS 选择器开始!(^_^)

CSS 伪类选择器

CSS 选择发展到今日,可以说是一个庞大的体系了: ​

特别声明,上图来自于 @linxz 的博客如果上图看不清楚可以点击这里获取原图

现在描述 CSS 选择器的规范主要有 CSS (指的是 CSS 2.1),后面分为选择器 Level 3Level 4。伪类选择器在这三部分规范中都有出现过,他们是一个递进的过程,特别是在 Level 3Level 4,在原有的基础上新增了不少优秀的选择器。今天要提到的几个现代伪类选择器就是出于 Levele 4: ​

:is() 和 :where()

先来看 :is():where()@Elad Shechter 曾在推特上发了一个关于 :is():where() 选择器的测试题: ​

如果你是第一次看到这样的测试题,请先自测一下,如果是你,你会选择哪个答案(greenpurplered 还是 blue)?如果你选择的是 purple ,那么要恭喜你,你答对了。 ​

Demo 地址:codepen.io/airen/full/…

示例中出现了 :is():where() 两个可能你从未接触过的伪类选择器: ​

:is(.header, .main) .p {
	color: purple
}

:where(.header, .main) .p {
	color: red;
}
复制代码
复制代码

其实这两个选择器等同于: ​

.header .p,
.main .p {
	color: purple;
}

.header .p,
.main .p {
	color: red;
}
复制代码
复制代码

他们唯一不同之处,就是选择器权重不同。:where() 的优先级总是为 0 ,但是 :is() 的优先级是由它的选择器列表中优先级最高的选择器决定的。那么上面的示例中,我们可以用下图来清晰的描述他们之间的关系: ​

上图地址来自: twitter.com/eladsc/stat…

即,下面三个选择器选中的相同的元素: ​

.header .p,
.main .p {
	// ...
}

:is(.header, .main) .p {
	// ...
}

:where(.header, .main) .p {
	// ...
}
复制代码
复制代码

不同的是他们的权重不同,其中: ​

.header .p,
.main .p {
	// ....
}

:is(.header, .main) .p {
	// ...
}
复制代码
复制代码

示例中的两种选择器具有相同的权重(即020);其中: ​

:where(.header, .main) .p {
	// ...
}

.p {
	// ...
}
复制代码
复制代码

上面示例代码中的选择器具有相同权重(即 010): ​

:is():where() 伪类选择器的出现,将会让我们的选择器变得更简洁,比如下成这个 :is() 的示例: ​

// Level 0
h1 {
  font-size: 30px;
}
//  Level 1 
section h1, article h1, aside h1, nav h1 {
  font-size: 25px;
}
// Level 2 
section section h1, section article h1, section aside h1, section nav h1,
article section h1, article article h1, article aside h1, article nav h1,
aside section h1, aside article h1, aside aside h1, aside nav h1,
nav section h1, nav article h1, nav aside h1, nav nav h1, {
  font-size: 20px;
}
复制代码
复制代码

使用 :is() 可以像下面这样来描述:

// Level 0
h1 {
  font-size: 30px;
}
// Level 1
:is(section, article, aside, nav) h1 {
  font-size: 25px;
}
// Level 2 
:is(section, article, aside, nav)
:is(section, article, aside, nav) h1 {
  font-size: 20px;
}
复制代码
复制代码

回过头来,请大家再看看下面这个测试题,请问 p 文本的颜色是什么? ​

<!-- HTML -->
<main class="main">
	<p class="p">What is the text color?</p>
</main>

// CSS
:where(.header, .main) .p {
	color: red;
}

.p {
	color: blue;
}

:is(.header, .main) .p {
	color: purple;
}

.header .p,
.main .p {
	color: green;
}
复制代码
复制代码

可以点击这里查看答案: codepen.io/airen/full/…

:not() 和 :has()

或许大家平时在开发前端页面的时候,碰到类似下图这样的需求: ​

希望最后一张卡片没有margin-bottom (或第一张卡片没有 margin-top)。针对于这样的场景,:not() 选择器就非常有优势。为什么这么说呢? ​

在没有 :not() 选择器的时候,你可能会想到下面这样的方式: ​

.card + .card {
	margin-top: 20px;
}	

// 或

.card {
	margin-bottom: 20px;
}

.card.card--last { // 也可能会使用 .card:last-child
	margin-bottom: 0;
}
复制代码
复制代码

如果换成 :not() 选择器,可以这要来实现: ​

.card:not(:last-child) {
	margin-bottom: 20px
}
复制代码
复制代码

效果如下:

Demo URL: codepen.io/airen/full/…

虽然 CSS 选择器已经非常强大了,但一直以来,在 CSS 中没有从子元素选到父元素的样的选择器(父选择器): ​

有人提出希望有一个 :parents() 这样的选择器!

虽然直到目前为止还没有 :parents() 选择器,但庆幸的是,:has() 选择器即将到来,它可以用来选择父级元素。目前 Igalia 公司正在为 Chrome 实现该选择器,其团队成员 Brian Kardell 新发表的《Can I :has()》文章中对 :has() 选择器进行了详细的阐述。 ​

<section><!-- section 边框颜色是 blue,margin-bottom是 30px --> 
  <h1>H1 Level Title</h1>
</section>  

<section><!-- section 边框颜色是 #09f,margin-bottom是 30px --> 
  <h1>H2 Level Title</h1>
</section>  

<section><!-- section 边框颜色是 red --> 
  <p>Text Paragraphs</p>
</section>  

/* CSS */

// 将匹配含有h1子元素的 section元素
section:has(h1) {
	border-color: blue;
}
// 将匹配含有h2子元素的 section元素
section:has(h2) {
	border-color: #09f;
}
// 将匹配含有p子元素的 section元素
section:has(p) {
	border-color: red;
}
// 将匹配除了含有p子元素的 section元素
section:not(:has(p)) {
	margin-bottom: 30px;
}
复制代码
复制代码

在支持:has()的浏览器中,你将看到下图这样的效果: ​

示例中还演示了 :not():has() 组合在一起使用的,但两者组合在一起所达到的意思却完全不一样。其中 :not(:has(selector)) 匹配不含有 selector 元素的父元素,而 :has(:not(selector)) 匹配含有的不是 selector 子元素的元素。两者主要区别在于,:has(:not(selector)) 写法必须要含有一个子元素,而 :not(:has()) 可以不含有元素也会被匹配。 ​

有意思的是,Github 中有一个 Issue 在讨论** **:has-child()** 选择器**,或许哪一天,我们在组件中就可以这样使用: ​

:empty 和 :blank

在现代 Web 的开发的过程中,总是无法避免数据吐出为空的情况,此时往往会给我们的 UI 带来额外的麻烦。比如说,在同一类的 UI 元件中编写了一定的样式规则,但数据为空,此时在页面上可能会出现这样的场景: ​

​ CSS 的 `:empty` 和 `:blank` 两个伪类选择器可以帮助我们避免这种现象。这两个选择器都很有用: ​

  • 给空元素添加样式
  • 创建空的状态

既然都是可以为空元素添加样式,那何为空元素,他们之间的差异又是什么?先来回答第一个问题,何为空元素?空元素是指元素没有任何子元素或子节点,比如: ​

<!-- 空元素 -->
<div class="error"></div>
<div class="error"><!-- 注释 --></div>

<!-- 非空元素 -->
<div class="error"> </div><!-- 中间有一个空格符 -->
<div class="error">
</div><!-- 断行 -->
<div class="error">
	<!-- 注释 -->
</div><!-- 注释断行排列 -->
<div class="error"><span></span></div>
复制代码
复制代码

:empty:blank 相比,:empty 只能选中没有子元素的元素。子元素只可以是元素节点或文本(包括空格)。注释或处理指令都不会产生影响。 ​

Demo: codepen.io/airen/full/…

注意,在空元素上即使使用伪元素 ::before::after 创建内容,也能被:empty 识别。 ​

:blank 要比 :empty 灵活地多,只要该元素中无任何子元素都能被识别。不过 W3C 规范对该伪类选择器的定义更趋向于作用到表单控件中,比如用户没有在 inputtextarea 中输入内容时,提交表单能被识别到。有点类似于表单验证的功能。 ​

早在 2018 年 Zell Liew 在推特上就针对 :empty:blank 做过相关的讨论,并发表一了篇有关于这方面的博文《:empty and :blank》:

到目前为止,:empty 已得到主流浏览器支持,可以用于实际生产中,但 :blank 伪类选择器还是存在一定的争议的,有关于这方面的讨论可以点击这里查阅

:focus-visible 和 :focus-within

CSSer 对于 :focus 应该不会感到陌生,早期对于可聚焦元素可以使用 :focus 来给元素设置焦点状态下的 UI 风格,即焦点环样式: ​

Chrome 86 开始,另外引入了 :focus-visible:focus-within 两种伪类选择器来帮助我们更好的控制 UI 焦点环的样式: ​

虽然 :focus:focus-within:focus-visible 都可以用来设置焦点环的样式,但他们之间还是有一定的差异: ​

  • :focus :当用户使用鼠标点击焦点元素或使用键盘的 Tab 键(或快捷键)触发焦点元素焦点环的样式
  • :focus-visible :只有使用键盘的 Tab 键(或快捷键)触发焦点元素焦点环的样式。如果仅使用 :focus-visible 设置焦点环样式的话,那么用户使用鼠标点击焦点元素时不会触发焦点环样式
  • :focus-within:表示一个元素获得焦点,或该元素的后代元素获得焦点。这也意味着,它或它的后代获得焦点,都可以触发 :focus-within

来简单的看一个示例: ​

button:focus { 
  outline: 2px dotted #09f; 
  outline-offset: 2px; 
} 

button:focus-visible { 
  outline: 2px solid #f36; 
  outline-offset: 2px; 
}
复制代码
复制代码

你会发现,分别使用鼠标点击按钮和按Tab 让按钮获得焦点时焦点环样式效果不同: ​

不过需要注意的是,:focus:focus-visible 也会涉及到选择器权重的问题,就上面的示例来说,如果我们把 :focus 选择器对应的样式放置到 :focus-visible 之后: ​

button:focus-visible { 
  outline: 2px solid #f36; 
  outline-offset: 2px; 
}

button:focus { 
  outline: 2px dotted #09f; 
  outline-offset: 2px; 
} 
复制代码
复制代码

这个时候,你会发现不管用户使用键盘 Tab 键还是鼠标让 <button> 获得焦点时,焦点样式都会采用 :focus 对应的样式: ​

如果我们要让 :focus:focus-visible 可以有独自的样式,可以借助CSS选择器中的 :not() 来处理: ​

button:focus:not(:focus-visible) {
  outline: 2px dotted #416dea;
  outline-offset: 2px;
  box-shadow: 0px 1px 1px #416dea;
}

button:focus-visible {
  outline: 2px solid #416dea;
  outline-offset: 2px;
  box-shadow: 0px 1px 1px #416dea;
}
复制代码
复制代码

这个时候按 Tab 键盘和鼠标点击时焦点环样式就有相应的差异: ​

Demo: codepen.io/airen/full/…

或许你想到这样的场景了,如果你在做 A11Y 方面的优化,希望在移动端和PC端上对焦点元素设置不同的焦点环样式,那用上面这种方案来说就会非常的灵活。 ​

对于 :focus-within 而言,它其实有点类似于 :has() 伪类选择器,可以在子元素得到焦点时,触发父元素: ​

上图描述了 :focus-within:focus 之间的差异。更简单的说,它有点类似于 JavaScript 的事件冒泡,从可获得焦点元素开始一直冒泡到HTML的根元素 <html> ,都可以接收触发 :focus-within 事件。比如: ​

.box:focus-within,
.container:focus-within {
    box-shadow: 0 0 5px 3px rgb(255 255 255 / 0.65);
}

body:focus-within {
    background-color: #2a0f5bde;
}

html:focus-within {
    border: 5px solid #09f;
}
复制代码
复制代码

Demo: codepen.io/airen/full/…

借助于 :focus-within 伪类选择器的特性,我们就可以让一些交互效果变得更为简单,比如下面这个示例,使用 :focus-within 就可以实现,不再需要任何 JavaScript 代码: ​

Demo: codepen.io/airen/full/…

有了 :focus:focus-visible:focus-within 会让我们更好的管理焦点元素在焦点状态下焦点环的 UI 效果。 ​

在 CSS 的世界中,除了这里提到的伪类选择器之外,还有很多其他的伪类选择器(或伪元素),比如 ::marker:in-range:out-of-range 。 ​

如果你感兴趣的话,可以尝试着使用 :in-range:out-of-range 给一些 input (比如 typenumberrangeinput 元素,他们都有 minmax 属性的设置) 在用户输入范围值和非范围值时,提供不同的 UI 效果。 ​

CSS 颜色

CSS 颜色模块自从 Level 4 开始,除了新增了一些新的函数值,比如 hwb()lch()lab()color-mix()color-contrast()color() 之外,对原本的 rgb()hsl() 以及 #rrggbb 等颜色值定义语法规则也做出调整。 ​

比如,原本我们熟悉的描述颜色值的方式:

#09f 
#90eafe
rgb(123, 123, 123)
rgba(123, 123, 123, .5)
hsl(220, 50%, 50%)
hsla(220, 50%, 50%, .5)
复制代码
复制代码

都有了新的语法规则,特别是对于 rgb()rgba()hsl()hsla() ,以往用逗号(,)作为分割符,现在可以直接使用空格做分割符,并且rgb()hsl() 函数在第三个值和第四个值之间加上 / 可以取替 rgba()hsla() : ​

另外,十六进制描述颜色,也可以在原本的语法规则中最后两位添加新的位值来描述透度明。比如 #rrggbbaa#rgba 。在Chrome 浏览器代码审查器中,已经可以看到这种语法规则的身影了: ​

也就是说,我们现在可以这样来描述颜色值: ​

#hex-with-alpha {
  color: #0f08;
  color: #00ff0088;
}

#functional-notation {
  color: hsl(0deg 0% 0%);
  color: hsl(2rad 50% 50% / 80%);
  color: rgb(0 0 0);
  color: rgb(255 255 255 / .25);
}

#lab-and-lch {
  --ux-gray: lch(50% 0 0);
  --rad-pink: lch(50% 200 230);
  --rad-pink: lab(150% 160 0);
  --pale-purple: lab(75% 50 -50);
}
复制代码
复制代码

另外,我们现在描述颜色都是在sRBG 色值空间,而颜色色值空间是一个复杂的体系,除了 sRGB 之外还有其他的色值空间,比如说 LCH: ​

正如上图所示,LCH 颜色空间的颜色数量要比 sRGB 颜色空间的多,而且描述的颜色更为细腻。 ​

如上图所示,可以在color() 函数中指定颜色空间: ​

#color-function {
  --rad-pink: color(display-p3 1 0 1);
  --rad-pink: color(lab 50% 150 -50);
  --rad-pink: color(srgb 100% 0% 50%);
}
复制代码
复制代码

注意,使用了color() 函数指定颜色空间除了要考虑该函数支持度(浏览器的兼容性)还需要考虑硬件设备对颜色空间的支持度。 ​

在 CSS 中可以借助媒体查询 @media 来做相应的判断,比如下面的示例,如果终端设备支持的话,就会采用指定颜色空间的色值: ​

@media (dynamic-range: high) {
  .neon-pink {
    --neon-glow: color(display-p3 1 0 1);
  }
  
  .neon-green {
  	--neon-grow: color(display-p3 0 1 0);
  }
}

复制代码
复制代码

更为强大的是 CSS 颜色模块 Level 5 版本,对颜色函数能力做了进一步的扩展。比如,可以在 rgb()hsl()hwb()lab()lch() 函数基础上添加 from 关键词,实现基于一个颜色的基础上,只对某个参数做调整: ​

rgb() = rgb([from <color>]? <percentage>{3} [ / <alpha-value> ]? ) | rgb([from <color>]? <number>{3} [ / <alpha-value> ]? ) 
hsl() = hsl([from <color>]? <hue> <percentage> <percentage> [ / <alpha-value> ]? ) 
hwb() = hwb([from <color>]? <hue> <percentage> <percentage> [ / <alpha-value> ]? ) 
lab() = lab([from <color>]? <percentage> <number> <number> [ / <alpha-value> ]? ) 
lch() = lch([from <color>]? <percentage> <number> <hue> [ / <alpha-value> ]? )
复制代码
复制代码

来看一个 hsl() 的示例: ​

上图展示的是,基于 --theme-primary (原色,即 hsl(274, 61%, 50%))颜色只对l 参数做调整,从 50% 调整到 30% ,从而获得一个新的颜色,即 hsl(274, 61%, 30%) 。使用这样的方式,我们就可以很轻易的获取基于某个颜色参数改变得来的颜色面板: ​

除此之外, 颜色模块 Level 5 版本还新增了一些新的函数用来描述颜色,比如 color-mix()color-contrast()color-adjust() 等: ​

.color-mix {
  --pink: color-mix(red, white);

  --brand: #0af;
  --text1: color-mix(var(--brand) 25%, black);
  --text2: color-mix(var(--brand) 40%, black);
}

.color-contrast {
  color: color-contrast(
    var(--bg)
    vs
    black, white
  );
}

--text-on-bg: color-contrast(
  var(--bg-blue-1)
  vs
  var(--text-subdued),
  var(--text-light),
  var(--text-lightest)
);

.color-adjust {
  --brand: #0af;
  --darker: color-adjust(var(--brand) lightness -50%);
  --lighter: color-adjust(var(--brand) lightness +50%);
}
复制代码
复制代码

简单介绍一下这几个颜色函数。其中 color-mix() 函数接受两个 <color> 值,并在给定的颜色空间中以指定的数量返回它们混合的结果。如果没有特殊说明,否则在 lch() 颜色空间中进行混合。比如下图所展示的就是 在lch() 颜色空间(默认)中将 redyellow 混合,每个 lch 通道的红色值占65%,黄色值占 35%,即 color-mix(red yellow 65%)

color-contrast() 函数很有意思,它可以帮助我们提高Web可访问性方面的能力。其主要作用是获取一个颜色值,并将其与其他颜色值的列表进行比较,从列表中选择对比度最高的一个。 ​

比如 color-contrast(white vs red, white, green) ,分别会拿 redwhitegreenwhite 对比,其中 greenwhite 对比度最高,最终会取 green 颜色: ​

color-adjust() 函数可用来在给定颜色空间中通过指定的变换函数对该颜色进行调整,除非另有规定,否则调整是在 lch() 色彩空间中进行的。比如 color-adjust(#0af lightness +50%) 是颜色 #0aflch 颜色空间中高度增加 50%

也就是说,在不久的未来,这些颜色函数可以很好的帮助我们控制Web中的颜色值。因为这些颜色函数都已成为主流浏览器的实验性属性,特别是 Safari 浏览器,对这方面的支持度要高于 Chrome 和 Firefox 浏览器。 ​

CSS 背景

CSS 的 background 属性是一个简写属性,这个对于大家来说再熟悉不过了。不过在这里着重想把 background-positionbackground-repeat 单独拿出来和大家聊聊。你可能会问,这两个属性又不是什么新属性了,有什么值得聊的呢?其实未必,这两个属性虽然老,但有几个小细节对于很多开发者来说还是会感到陌生的。咱们先从 backgroundd-position 开始。 ​

background-position

background-position 主要是用来指定背景图片在容器中的位置。大家较为熟悉的是,背景图片左上角(顶点)相对于容器左上角计算,如下图所示: ​

不过,有的时候,希望背景图片能相对于容器右侧边缘或底部边缘计算,比如下图: ​

要让背景图片距离容器右侧边缘和底部边缘都是 50px 。针对这样的场景,你或许首先会想到使用容器大小和背景图片大小进行计算,得出距离顶部和左侧边缘的距离,然后将计算出来的值运用于 background-position 中。当然,熟悉 CSS 的同学或许会想到使用 calc() 函数: ​

:root {
	--xPosition: 50px;
  --yPosition: 50px;
}

.container {
	background-position: calc(100% - var(--xPosition)  calc(100% - var(--yPosition)))
}
复制代码
复制代码

使用 calc() 动态计算要比通过容器和背景图片尺寸大小计算方便得多。事实上呢?background-position 提供了另一种特性,我们可以通过关键词 toprightbottomleft 来指定方向。比如我们熟悉的用法 : ​

background-position: 50px 50px;

// 相当于
background-position: top 50px  left 50px;
复制代码
复制代码

那么,我们要实现上图的效果,就可以使用 rightbottom 关键词,让事情变得非常简单: ​

:root {
	--xPosition: 50px;
  --yPosition: 50px;
}

.container {
	background-position: right var(--xPosition)  bottom var(--yPosition);
}
复制代码
复制代码

最终效果如下:

Demo: codepen.io/airen/full/…

注,示例中左侧是使用 calc() 计算的效果,右侧使用的是关键词实现的效果。 ​

background-position 使用还有一个小细节需要注意,即 background-position 采用百分比值。因为使用百分比值的 background-position 计算会相对于其他单位值复杂: ​

正如上图所示,背景图片原始尺寸是 100px x 100px ,容器的尺寸(使用该背景图片的元素)是 410px x 210px ,如果使用 background-position: 75% 50% 时,它的计算如下: ​

// background-position的x轴坐标
x = (容器宽度 - 背景图片宽度) x background-position的x坐标的百分比值 = (410 - 100) x 75% = 232.5px

// background-position的y轴坐标
y = (容器高度 - 背景图片高度) x background-position的y坐标的百分比值 = (210 - 100) x 50% = 55px
复制代码
复制代码

就该示例而言,background-position: 75% 50% 就相当于 bacckground-position: 232.5px 55px。 ​

注意,如果我们把 background-sizebackground-position 取百分比值场景结合起来使用的时候,会让事情变得更为复杂,特别是background-size取值为covercontain 更会让你感到蛋疼。为什么会这样呢?这已经超出来这篇文章所要探讨的话题,如果你感兴趣的话,可以以此为课题,深入探讨一下。 ​

background-repeat

background-repeat 属性上除了可以使用我们熟悉的 no-repeatrepeat (或者 repeat-xrepeat-y) 之外还可以使用 roundspace 。 ​

我们都知道,使用 repeat 的时候,有可能会造成背景图片在平铺的时候被裁剪。如果希望背景图片在平铺时不被剪切,那么可以使用 round 来替代,它的最大特色就是背景图片在平铺的时候会根据容器的宽高对背景图片做相应的尺寸调整。而 space 会留出相应的空间,即在保证背景图片不被裁剪的情况之下,对多出来的空间以空白的方式在背景图片之间留出。 ​

针对于这些值,我录制了一个简单的视频,从视频的展示效果中可以更好的看出它们之间的差异: ​


Demo: codepen.io/airen/full/…

CSS 蒙层和剪切

如果你对设计或设计软件较为熟悉的话,对于蒙层和剪切不会感到陌生。设计师在做一些设计稿的时候,时常也会用到蒙层和剪切的能力。随着 CSS 的发展,在 CSS 的世界中也有了这两个特性,它们在 W3C 的 《CSS Masking Module Level 1》规范中定义,主要的作用如下图所示: ​

可以灵活的控制内容的显示区域。 ​

蒙层和剪切对应的 CSS 属性就是 maskclip-path ,其中 mask 是一个简写属性,它的使用规则和 background 非常的相似。我将通过简单的示例来向大家展示它们能帮我们做些什么。 ​

先看 mask : ​

Demo: codepen.io/airen/full/…

视频中的 emoji 和文本残缺不全(看上去被啃了一样)。在没有mask 的能力之前,如果我们要实现这样的效果几乎是不太可能,现在有了之后,实现起来就非常的简单。我们只需要像下面这样的一张图片(用于mask-image 上的图片),即蒙层图: ​

h1 {
	mask-image: url(mask.png);
}
复制代码
复制代码

而且我们还可以借助 mask 的合成能力,让多个蒙层做合成运算: ​

运用蒙层相关的能力,可以快速帮助我们实现一些业务场景所需的效果:

除此之外还可以实现换肤的效果:

Demo: codepen.io/airen/full/…

再来看剪切。 在 CSS 中使用 clip-path 的能力,除了可以帮助我们控制好所要展示的区域之外,还可以结合不同的路径和函数值实现不规则的图形展示。比如 Clippy 所展示的效果: ​

比如在实际需求中,需要实现一些不规则,又是多状态下的 UI 效果,那么 clip-path 就会很方便: ​

而且使用clip-path 结合 transitionanimation 可以在交互上做一些更好的动效: ​

左侧是原状态下效果,右侧是鼠标悬浮状态下效果: ​

Demo: codepen.io/airen/full/…

特别是,当clip-path 支持 path() 函数时,可以做得事情更多了,比如我们想要绘制一个心形的 UI 效果: ​

另外就是把 clip-pathfloat 和 CSS 的 Shapes 结合可以实现一些不规则的图文混排的布局效果: ​

CSS 混合模式

CSS 混合模式是个很有意思的特性,目前主要有 mix-blend-modebackground-blend-mode 两个属性,前者是用于多个元素的合成,后者是用于多个背景的合成。使用它们可以实现一些特殊的效果,比如类似 Photoshop 中的滤镜效果: ​

采用混合模式特性,我们可以轻易的实现产品图换色的效果: ​

Demo: codepen.io/kylewetton/…

这里简单地介绍一下,这个效果是怎么实现的。 ​

首先我们有一张类似下图的产品图: ​

通过 SVG 的能力,我们描绘图一个纯黑色的 SVG 形状,形状和上图的沙发是相吻合的: ​

描绘出来的 SVG 代码并不复杂: ​

<svg id="js-couch" class="couch__overlay" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="none" width="1000" height="394"> 
  <defs> 
    <path d="M996.35 77.55q-1.85-1.95-8.65-3.75l-62.4-17.1q-9.3-2.75-12.15-2.5-1.8.15-2.85.45l-.75.3q2.25-16.3 3.75-22.05 1.15-4.4 1.4-10.8.2-6.6-.7-10.85-1.25-5.65-3.1-7.8-2.95-3.35-9.65-2.7-5.95.6-39.3 1.7-38.3 1.25-39.45 1.3-10.25.5-126.75.5-5.05 0-54.2 1.3-45.8 1.25-54.05.95-19.45-.45-30.4-.7-20.2-.55-23.1-1.3-22.3-5.85-26.5 1.25-2.65 4.55-3.85 7.9-.6 1.7-.7 2.5-.65-2.2-2.05-4.55-2.75-4.65-6.45-5.2-3.85-.55-13.65-.4-7.4.1-12 .4-.4.05-18.7.9-16.55.8-19.15 1.1-3.4.4-14.6 1.1-11.3.75-13.05.65h-9.8q-8.65-.05-11.45-.4-2.85-.35-9.25-.6-6.7-.15-8.5-.25-2.7-.1-27.75-.1-25.1 0-29.6.1-92.35 1.15-99 1.65-5.15.4-20 0-15.3-.4-24.4-1.25-6.75-.6-21-1.55-12.95-.9-14.85-1.1-6.45-1.05-11.05-1.5-8.7-.85-12.85.5-5.45 1.75-8.1 4.65-3.2 3.4-2.9 8.6.25 4.65 2.1 11.8 1 3.8 2.55 9.1 1 3.85 2.35 10.1-.1 1-1.5 1-1.75 0-7.7.85-7.1 1-9.8 2.05-2.4.9-23 4.75-21.2 3.9-22.05 4.15-8.2 1.85-15.05 3.35Q7.4 69.1 5.65 70.3 2.5 72.45 2 73.1.6 75 .75 79.2q.15 4.15 1.3 12.75.9 6.85 1.45 10 .5 2.75 8.55 54 6.65 42.15 7.35 46.85 1.15 7.65 4.9 28.55 4.55 25.2 6.35 31.2 2.45 8.15 3.8 11.75 1.85 4.9 3.2 5.75 1.25.8 6.85.65 2.75-.05 5.3-.25l23.85.35q.1 0 1 .95t2 .95q1.9 0 3.4-1.4l23.1-.25 43.65.4q135.05 2.15 137.9 1.9 1.25-.1 72.9.5 72.45.65 76.85.45 8.1-.35 64 .4 143.35.95 146 1.1.55.05 75.3.3 74.7.3 79.8.6 8.65.5 68.25-.35l51.75.5 1.6.4q1.95.35 3.8.05 1.45-.25 3.5-.2 1.9 0 3.35-.3 2.1-.45 8.25-.8 6.25-.3 8.75-.05 1.7.2 8 1 5.75.3 7.4-1.75 1.75-2.2 4.95-10.85 2.8-7.55 4.05-12.4.65-2.5 3.6-17.2 2.75-13.75 3.15-14.8.45-1.25 4.45-22.85 4.05-22.4 4.4-24.4.3-1.45 3.75-25.2 3.35-23.2 4-26.3 1.15-5.5 2.35-18.8 1.4-15.7.8-23.7-.6-8.35-3.35-11.15z" id="a" /> 
	</defs> 
  <use xlink:href="#a"/> 
</svg>
复制代码
复制代码

使用 CSS 自定义属性,配合少许的 JavaScript代码即可完成产品图换色的效果: ​

:root {
	--fill: #f36;
}

svg {
	fill: var(--fill);
  mix-blend-mode: multiply;
}
复制代码
复制代码

我们使用 mix-blend-modemultiply 效果。另外,JavaScript 动态改变 --fill 的值代码也简单: ​

const inputEle = document.querySelector('input') 

inputEle.addEventListener('change', (e)=>{ 
  document.documentElement.style.setProperty('--fill', e.target.value); 
})
复制代码
复制代码

很简单吧。而且你可以发挥你的想象空间,可以实现很多具有创意的效果。 ​

CSS 自定义属性

CSS 自定义属性可以说是 CSS 的标配了,正如 W3C 规范所描述的,CSS 自定义属性又被称为 CSS 变量: ​

不过在这里要说的不是 CSS 自定义如何使用?要和大家说的是 CSS 自定义属性中的无效变量。CSS 自定义属性中的无效变量是很有用的一个特性,它可以实现 1 (真)和 0 (假)的开关切换效果。换句话说,可以很好的实现不同状态的 UI 效果。 ​

同样地,在 W3C 规范中对无效变量有详细描述,但不仔细的话,还是会忽略该特性。先来看规范是怎么描述无效变量的: ​

它的意思是: ​

当一个自定义属性的值是 **initial** 时,**var()**** 函数不能使用它进行替换。除非 指定了一个有效的回退值,否则会使声明在计算值时无效。 **

在 CSS 中注册自定义属性时是使用 -- 来注册的,可以给已注册的自定义属性赋值,包括空字符串,但其中有一个细节非常有意思: ​

:root {
	--invalid:; // 注意冒号和分号之间无空格符,也无任何字符
  --valid: ;  // 注意冒号和分号之间只有一个空格符
}
复制代码
复制代码

其中,--invalid 自定义属性被称为无效变量,而 --valid 自定义属性是一个有效变量。使用上面这种方式来区分有效和无效变量对于开发者而言,可读性极差,而且有些文本编辑器可能会对代码按自己配置的规格格式化,有可能会造成 --invalid:; 变成 --invalid: ; (有空格)。为此,一般使用关键词 initial来显式声明一个自定义属性为无效变量:

:root {
	--invalid: initial; // 无效变量
  --valid: ;					// 有效变量
}
复制代码
复制代码

如果不使用 var() 函数调用已注册的自定义属性的话,那么对于已注册的自定义属性而言,不会起任何作用。而 var() 函数有两个参数,第一个参数就是自定义属性,第二个参数是备用值。当第一个参数是个无效值时,会采用第二个参数。正因如此,对于已注册的无效自定义属性(即无效变量),比如 上面代码中的 --invalid 。那么 var() 没有提供备用值(第二个参数),则会使 CSS 样式规则(声明)在计算值时无效: ​

:root {
	--invalid: initial; // 无效变量
  --valid: ;          // 有效变量
}

foo {
	background-color: var(--invalid); // 未提供备用值,则background-color 计算值无效
  color: var(--invalid, red)        // 提供了备用值,--invalid是无效变量,则会采用备用值 red
}
复制代码
复制代码

先来看一个简单的示例: ​

<div class="element">
  <i>Element</i>
</div>

<i>Element</i>

<style>
  .element {
  	--color: red;  // 只作用于 .element 元素及其后代元素
	}

	i {
  	--foo: initial;                         // 无效变量
  	--color: var(--foo);										// 无效变量
  	background-color: var(--color, orange); // --color是无效变量,采用备用值 orange
	}
</style>
复制代码
复制代码

运行上面的代码,你将看到的效果如下图所示: ​

Demo: codepen.io/airen/full/…

再来看另一个示例: ​

<section>Element</section>

<style>
  :root {
    --initial: initial; // 无效变量
    --valid: ;					// 有效变量
	}
  section {
  	background-color: var(--initial, red); // 无效变量,会采用备用值 red
    color: var(--valid, red);							 //	有效变量,备用值被忽略
  }
</style>  
复制代码
复制代码

因此,你看到的效果如下: ​

Demo: codepen.io/airen/full/…

这个示例中 color 属性相当于设置了 color: ; 值,客户端在计算该值时将被忽略,因此会继承其祖先元素的 color 值,该例继承了 body 元素的 colorblack 。 ​

你可能对 CSS 自定义属性中的无效变量有了一定的认知。我们继续往下。 ​

为了在使用 CSS 自定义属性中的无效变量让开发者更易于理解,@Lea Verou在《The --var: ; hack to toggle multiple values with one custom property》引入了类似于Switch的概念,即: ​

:root {
	--ON: initial; // 无效变量,相当于开启 var()的备用值
  --OFF:;        // 有效变量,相当于关闭 var()的备用值
}
复制代码
复制代码

这样一来,我们在做 UI 不同状态切换时,就只需要对 --ON--OFF 的切换。比如 @Lea Verou 在文章中提供的示例:

:root {
  --ON: initial;
  --OFF: ;
}

button {
  --is-raised: var(--OFF);
  
  border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
  background: var(
      --is-raised,
      linear-gradient(hsl(0 0% 100% / 0.3), transparent)
    )
    hsl(200 100% 50%);
  box-shadow: var(
    --is-raised,
    0 1px hsl(0 0% 100% / 0.8) inset,
    0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
  );
  text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}

button:hover {
  --is-raised: var(--ON);
}

button:active {
  box-shadow: var(--is-raised, 0 1px 0.2em black inset);
}
复制代码
复制代码

最终效果如下: ​

Demo: codepen.io/airen/full/…

上面这个示例算是简单的了。不过我们可以利用该特性实现更复杂的 UI 效果。比如下面这个多种换肤效果: ​

label {
  --box-shadow: var(--ON);
  --box-shadow-active: var(--OFF);
  box-shadow: 0 0 0 3px var(--box-shadow, rgba(0, 0, 0, 0.05))
    var(--box-shadow-active, #2196f3);
  cursor: pointer;
}

label.dark {
  background-color: var(--dark-bgcolor);
}

label.light {
  background-color: var(--light-bgcolor);
}

label.blue {
  background-color: var(--blue-bgcolor);
}

#dark:checked ~ div .dark,
#light:checked ~ div .light,
#blue:checked ~ div .blue {
  --box-shadow: var(--OFF);
  --box-shadow-active: var(--ON);
}

.nav {
  color: var(--light, var(--light-color)) var(--dark, var(--dark-color))
    var(--blue, var(--blue-color));
  background-color: var(--light, var(--light-bgcolor))
    var(--dark, var(--dark-bgcolor)) var(--blue, var(--blue-bgcolor));
}

a.active,
a:hover {
  background-color: var(--light, var(--light-active-bgcolor))
    var(--dark, var(--dark-active-bgcolor))
    var(--blue, var(--blue-active-bgcolor));
}

/* 设置切换开关 */
:root {
  --ON: initial;
  --OFF: ;

  /* Dark */
  --dark-color: rgba(156, 163, 175, 1);
  --dark-bgcolor: rgba(17, 24, 39, 1);
  --dark-active-bgcolor: rgba(55, 65, 81, 1);

  /* Light */
  --light-color: rgba(55, 65, 81, 1);
  --light-bgcolor: rgba(243, 244, 246, 1);
  --light-active-bgcolor: rgba(209, 213, 219, 1);

  /* Blue */
  --blue-color: rgba(165, 180, 252, 1);
  --blue-bgcolor: rgba(49, 46, 129, 1);
  --blue-active-bgcolor: rgba(67, 56, 202, 1);
}

#dark:checked ~ .nav {
  --light: var(--OFF);
  --dark: var(--ON);
  --blue: var(--OFF);
}

/* 默认为Light */
#light:checked ~ .nav {
  --light: var(--ON);
  --dark: var(--OFF);
  --blue: var(--OFF);
}

#blue:checked ~ .nav {
  --light: var(--OFF);
  --dark: var(--OFF);
  --blue: var(--ON);
}
复制代码
复制代码

效果如下: ​

Demo: codepen.io/airen/full/…

​示例中没有使用任何 JavaScript 代码。是不是很有意思。要是感兴趣的话,你也可以尝试着撸撸这样的效果。

文章分类
前端
文章标签