现代 CSS

2020年CSS有哪些新特性

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

今天在Twitter上看到@argyleink伦敦(LondonCSS 2020)CSS四次活动中分享的一个话题《What's new with CSS?》。看了一下这个主题的PPT,里面有些新东西还是蛮有意思的,稍微整理一下和大家一起分享。如果你感兴趣的话,请继续往下阅读。

主题PPT

这个主题是@argyleink大神分享的,对应的PPT可以扫下面这个二维码:

或者点击这里访问PPT

这次活动除了这个话题还有另外两个话题:

我们回到今天的主题:2020年CSS有哪些新特性

动态模糊(Motion Blur)

Motion Blur(动态模糊),这个概念也是我第一次接触的。查了一些资料才明白,动态模糊是一种模糊效果,它只在特动移动的时候才会让物体模糊,通常这种模糊是在物体移动的方向上应用的,这就是当你试图在拍一个移动的物体的效果。

当你拍摄一个物体(或是一个人),就像下图,这个模糊就会发生,因为这个物体(人运动)移动的速度超过了相机拍摄所需的曝光时间,所以这个物体会在最终的照片中出现多次,因为它在关键时刻移动。

在我们平时制作动效的时候,很少会考虑(添加)动态模糊效果,另外在CSS和SVG动画中也缺少这方面的效果,因此大多数Web动画看起来很生硬。也就是说,如果我们给动画在运动过程中添加一些模糊效果,会让这种动效更贴近现实生活。

为此,@argyleink向W3C规范提出了有关于动态模糊相关的实现方案(建议),他为开发者(CSSer)推荐了一种方法,在CSS的animation是新增了 motion-renderingmotion-shutter-angle

.animated-layer {
    /* GPU加速动画 */
    animation: rotate .5s linear infinite;

    /* 向引擎请求动态模糊
     * motion-rendering可以接受inherit | initial | auto | none | blur 值
    */
    motion-rendering: blur; 

    /* 类似于相机的快门,指的是快门角度,用来控制模糊量或模糊强度
     * motion-shutter-angle可受任意角度值 inherit | initial | auto = 180deg | [0deg, ..., 720deg]
    */
    motion-shutter-angle: 180deg;
}

@keyframes rotate {
    to {
        transform: rotate(1turn);
    }
}

来看一个简单的示例:

从效果上看似乎没看到动态模糊的效果。

这里还是用文档中提供的一个带有动效的图向大家展示动态模糊的效果:

虽然说, motion-renderingmotion-shutter-angle 还只是一个提案,离到TR阶段还需要很长的时间,但对于某些场景(动画场景),可以使用CSS的filter来模拟,比如下面这个效果:

代码很简单:

.blur { 
    animation: blur 250ms; 
}

@keyframes blur {
    0% { 
        -webkit-filter: blur(0px); 
    }
    50% { 
        -webkit-filter: blur(5px); 
    }
    100% { 
        -webkit-filter: blur(0px); 
    }
}

另外,现在提供的 motion-renderingmotion-shutter-angle 还只是一个提议,在Github中讨论的评论中,也有建议将这两个属性换成:

filter: motion-blur(5px) motion-squash(2px)

// 或
transform-fiilter: motion-blur(180deg)

// 或
transition-filter: motion-blur(180deg)

也就是说,上面提供的CSS实现动态模糊的属性不是完全定下来的,随着后面的发展,CSS中实现动态模糊的属性,一切皆有可能。

再来看看SVG世界中,相对于CSS世界而言,SVG中要实现动态模拟效果要更容易一些,可以使用SVG中的滤镜来模拟动态模糊效果:

上图来自于@Lucas Bebber的《Motion Blur Effect with SVG》教程。

如果你对SVG中的滤镜相关的知识感兴趣的话,可以阅读:

有意思的是,@Michelle Barker在Codepen写了一个Demo,这个Demo是用CSS的box-shadow模拟出有动态模拟的效果:

如果你真的想在项目中让自己的动画效果具有动态模糊效果(让动效看上去更真一点),而又担心CSS或SVG相关特性未得到主流浏览器支持而不敢使用,那么我在这里向大家推荐一个JavaScript库: MotionBlurJS

来看使用MotionBlurJS实现的动态模糊效果:

@scroll-timeline

Web开发者时常会碰到使用滚动来触发某些元素的动画效果,比如说,页面滚动条滚动到某个位置,标题固定在顶部;页面顶部展示你页面进度(滚动指示器);还是一些我们所说的视差滚动效果等。以往实现这些效果,大都借助JavaScript来实现,可以通过DOM事件查看滚动位置,并根据该位置更改元素的样式。如果可以的话,最好使用IntersectionObserver。有关于这方面的介绍可以阅读:

不过,现在有一个关于这方面的CSS草案,即 Scroll-linked Animations。也就是说,在未来,我们可以直接使用CSS的@scroll-timeline属性来实现前面提到的一些动画效果。

@scroll-timeline = @scroll-timeline <timeline-name> { <declaration-list> }

在规范中向大家提供了两个简单的示例,比如说,两个圆球的碰撞动效。这个动效由滚动位置来控制,简单地说,随着页面往下滚动,左右两个球慢慢向中间靠齐,直到他们两碰撞到一起,变成一个圆。反之,页面往上滚动时,中间的圆慢慢的会分离出左右两个圆。

CSS代码大致像下面这样:

@media (prefers-reduced-motion: no-preference) {
    div.circle {
        animation-duration: 1s;
        animation-timing-function: linear;
        animation-timeline: collision-timeline;
    }

    #left-circle {
        animation-name: left-circle;
    }

    #right-circle {
        animation-name: right-circle;
    }

    #union-circle {
        animation-name: union-circle;
        animation-fill-mode: forwards;
        animation-timeline: union-timeline;
    }

    @scroll-timeline collision-timeline {
        source: selector(#container);
        orientation: block;
        start:  200px;
        end: 300px;
    }

    @scroll-timeline union-timeline {
        source: selector(#container);
        orientation: block;
        start:  250px;
        end: 300px;
    }

    @keyframes left-circle {
        to { transform: translate(300px) }
    }

    @keyframes right-circle {
        to { transform: translate(350px) }
    }

    @keyframes union-circle {
        to { opacity: 1 }
    }
}

如果是使用JavaScript的话,可以像下面这样:

if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
    const scrollableElement = document.querySelector('#container');

    const collisionTimeline = new ScrollTimeline({
        source: scrollableElement,
        start: CSS.px(200),
        end: CSS.px(300)
    });

    const left = leftCircle.animate({ transform: 'translate(300px)' }, 1000);
    left.timeline = collisionTimeline;

    const right = leftCircle.animate({ transform: 'translate(350px)' }, 1000);
    right.timeline = collisionTimeline;

    const union = unionCircle.animate({ opacity: 1 }, { duration: 1000, fill: "forwards" });
    union.timeline = new ScrollTimeline({
        source: scrollableElement,
        start: CSS.px(250),
        end: CSS.px(300)
    });
}

再来看一个滚动计时器的效果:

上面的示例,我们是使用渐变来模拟的一个效果,但有了@scroll-timeline我们就可以像下面这样来实现:

@media (prefers-reduced-motion: no-preference) {
    @scroll-timeline progress-timeline {
        source: selector(#body);
        start: 0;
        end: 100%;
    }

    @keyframes progress {
        to { width: 100%; }
    }

    #progress {
        width: 0px;
        height: 30px;
        background: red;
        animation: 1s linear forwards progress progress-timeline;
    }
}

如果使用Web Animation API的话,可以像下面这样:

if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches) {
    var animation = div.animate({ width: '100%' }, { duration: 1000, fill: "forwards" });
    animation.timeline = new ScrollTimeline(
        {
            start: 0,
            end: CSS.percent(100)
        }
    );
}

@argyleink在他分享的PPT中也提供了一个简单的示例:

滚动页面的时候,你会发现两个数字之间的/符会不断的旋转:

上面的示例效果是基于Web Animation API来实现@scroll-timeline的效果,但是目前还需要其对应的Polyfill

import 'https://flackr.github.io/scroll-timeline/dist/scroll-timeline.js'

const counter = document.querySelector('main')
const slashes = document.querySelectorAll('.slash')

slashes.forEach(slash => { 
    slash.animate({
        transform: ['rotateZ(0)','rotateZ(4turn)']
    },{
        duration: 1000,
        fill: 'both',
        timeline: new ScrollTimeline({
            scrollSource: counter,
            fill: 'both',
        }),
    })
})

有关于@scroll-timeline更详细的介绍可以查阅 Scroll-linked Animations,另外,该规范目前还只是一个草案,在未来有可能还会有所变动。

leading-trimtext-edge

一直以来,在Web的排版中行高(line-height)总是令Web开发者感到困惑和头痛,主要是因为line-height在CSS中是一个非常复杂的体系。他的计算总是会涉及到很多因素:

@iamvdo的《Deep dive CSS: font metrics, line-height and vertical-align》一文对这方面做过深入的阐述!

在还原UI时,文本的行高总是让我们计算元素块之间的间距带来一定的麻烦:

为了解决这方面的烦恼, CSS Inline Layout Module Level 3新增了一个leading-trimtext-edge属性。可以让我们删除每一种字体中的额外间距,以便我们可以更好的计算相邻块元素之间的间距。

h1 { 
    leading-trim: both;
    text-edge: cap alphabetic;
}

上面的示例首先使用text-edge来告诉浏览器想要的文本边缘是cap高度和字母基线(alphabetic baseline)。然后用leading-trim对文本两边进行修剪。

注意,leading-trim只影响文本框,它不会切断其中的文字

示例中的两行简单的CSS创建了一个包含文本的干净的文本框(不受line-height相关的特性影响)。这有助于实现更精确的间距,并创建更好的视觉层次结构。

CSS的text-edgeleading-trim分别可接受的值:

text-edge: leading | [ text | cap | ex | ideographic | ideographic-ink ] [ text | alphabetic | ideographic | ideographic-ink ]?

leading-trim: normal | start | end | both

如果你对leading-trim特性感兴趣的话,除了阅读规范之外,还可以阅读下面几篇文章:

::grammar-error::spelling-error

::grammar-error::spelling-error是两个非常有意思的伪元素选择器。从字面说我们可以知道, Grammar error 指的是语法错误, Spelling error指的是拼写错误。 其实这两种现象在我们平时书写文本的时候经常可见,可能会由于手误, 将某个单词或标点符号用错,甚至语法上的错误。针对于这种现象,我们总是希望有一定的提示信息来提示我们,比如颜色上的差异,添加一些下划线等等:

CSS Pseudo-Elements Module Level 4高亮伪元素中我们可以看到这两个伪元素的身影:

  • ::grammar-error:浏览器为语法错误的文本段添加样式
  • ::spelling-error:浏览器为拼写错误的文本段添加样式

在CSS中并不是所有属性都能运用于这两个伪元素,到目前为止,只有colorbackground-colorcursortext-emphasis-colortext-shadowoutlinetext-decorationfill-colorstroke-colorstroke-width可以用于这两个伪元素。

:root::spelling-error { 
    text-decoration-line: spelling-error; 
}

:root::grammar-error  { 
    text-decoration-line: grammar-error; 
}

[spellcheck]::spelling-error {
    text-decoration: wavy underline var(--neon-red);
}

[grammarcheck]::grammar-error {
    text-decoration: wavy underline var(--neon-blue);
}

::cue::cue(selector)

::cue::cue(selector)对我而言是一个全新的东西,这两个伪元素是 WebVTT: The Web Video Text Tracks Format 模块中的。

::cue::cue(selector)两个伪元素最大的差别是后者带参数的伪元素。具体的作用:

  • ::cue伪元素(不带参数)匹配元素构造的任何WebVTT节点对象列表,但与背景符号对应的属性必须应用于WebVTT线索背景框,而不是WebVTT节点对象列表
  • ::cue(selector)是带有参数的伪元素,必须有一个由CSS选择器组成的参数。它匹配元素构造的WebVTT内部节点对象,该元素也匹配给定的CSS选择器

在CSS中只有部分属性可以运用于::cue::cue(selector)两个伪元素,比如coloropacityvisibilitytext-decorationtext-shadowbackgroundoutlinefontline-heightwhite-spacetext-combine-uprightruby-position等。

::cue {
    color: white;
    background-color: hsl(0 0% 0% / 90%);
}

说实话,没有完全阅读 WebVTT: The Web Video Text Tracks Format 模块所有内容,对其并不完全了解。这里只是做一个抛砖引玉的作用,如果你的工作内容和WebVTT相关,那应该对你会有一定的作用;如果你对这方面感兴趣的话,可以深挖这方面的知识。

:target:target-within

:target:target-within都是 Selectors Level 4 模块中的两个伪元素。可能很多同学对:target更熟悉一些,甚至用:target伪元素的特性制作了 TabAccordionModal 等UI交互效果。

比如下面这个手风琴的效果就是用:target伪元素制作的:

这里简单的来看看:target:target-within的作用。

在某些文档语言中,文档的URL可以通过URL的片段进一步指向文档中的特定元素。以这种方式指向的元素是文档的目标元素。其中片段标识符是URL中紧跟#的部分,例如#top#fontnote1。你可能已经使用它们创建页面内导航,比如大家常见的“跳转链接”。有了:target伪类,我们可以突出显示与该片段对应的文档部分,而且无需JavaScript也可以做到这一点。

比如下面这个简单的示例:

<!-- HTML -->
<h3>Table of Contents</h3>
<ol>
    <li><a href="#p1">Jump to the first paragraph!</a></li>
    <li><a href="#p2">Jump to the second paragraph!</a></li>
    <li><a href="#nowhere">This link goes nowhere, because the target doesn't exist.</a></li>
</ol>

<h3>My Fun Article</h3>
<p id="p1">You can target <i>this paragraph</i> using a URL fragment. Click on the link above to try out!</p>
<p id="p2">This is <i>another paragraph</i>, also accessible from the links above. Isn't that delightful?</p>

/* CSS */

p:target {
    background-color: gold;
}

/* 在目标元素中增加一个伪元素*/
p:target::before {
    font: 70% sans-serif;
    content: "►";
    color: limegreen;
    margin-right: .25em;
}

/*在目标元素中使用italic样式*/
p:target i {
    color: red;
}

点击示例中的链接,你可以看到像下图的效果:

:target-within伪类应用于:target伪类所应用的元素,以及在平面树(Flat Tree)的后代(包括非元素节点,比如文本节点)与匹配:target-within的条件相匹配的元素。

article:target-within {
    background-color: hsl(var(--surfaceHSL) / 10%);
}

其实在选择器Level 4模块中还新增了很多其他的伪类选择器,如果你对这方面新增的选择器感兴趣的话,可以阅读《初探CSS 选择器Level 4》一文,或者听一听@Adam Argyle和@Ana Tudor一起办的CSS Podcast,其中第十四期就是专门聊CSS的伪类选择器的

& >@nest

很多同学应该使用过像Sass、LESS之类的CSS处理器,这些处理器中有一个特大的特性就是选择器的嵌套,比如Sass中:

.parent {
    & > .child {
        color: red;
    }
}

.child {
    .parent & {
        color: blue;
    }
}

编译之后的CSS:

.parent > .child {
    color: red;
}

.parent .child {
    color: blue;
}

以往只能在CSS处理器中使用这样的特性,但将来在CSS中也可以使用这方面的特性,因为现在CSS中新增了一个嵌套模块,即 CSS Nesting Module。有点类似于CSS自定义属性(变量)特性一样,最早也是出现在CSS处理器中,现在原生CSS也支持了这方面的特性。

也就是说,在不久的将来(如果在你的工程构建中配置了postcss-preset-env,现在就可以使用):

article, section {
    &  p { 
        color: blue; 
    }
}

相当于:

:is(article, section) p { 
    color: blue; 
}

也就是:

article p,
section p {
    color: blue
}

还可以是& >结合起来使用:

article, section {
    & > p { 
        color: blue; 
    }
}

相当于:

article > p, 
section > p{
    color: blue; 
}

再来看另外几种有效的嵌套方式:

.foo {
    color: blue;
    & > .bar { 
        color: red; 
    }
}
/* 等同于 */
.foo { 
    color: blue; 
}
.foo > .bar { 
    color: red; 
}


.foo {
    color: blue;
    &.bar { 
        color: red; 
    }
}
/* 等同于 */
.foo { 
    color: blue; 
}
.foo.bar { 
    color: red; 
}


.foo, .bar {
    color: blue;
    & + .baz, &.qux { 
        color: red; 
    }
}
/* 等同于 */
.foo, .bar { 
    color: blue; 
}
:is(.foo, .bar) + .baz,
:is(.foo, .bar).qux { 
    color: red; 
}

但是下面这几种写法将是 无效的

/* 无效,因为没有嵌套选择器 */
.foo {
    color: red;
    .bar { 
        color: blue; 
    }
}

/* 无效,因为&不在第一个复合选择器中 */
.foo {
    color: red;
    .bar & { 
        color:blue; 
    }
}

/* 无效,因为列表中的第二个选择器不包含嵌套选择器 */
.foo {
    color: red;
    &.bar, .baz { 
        color: blue; 
    }
}

还可以结合 @nest 使用。下面这几种嵌套方式都是有效的:

.foo {
    color: red;
    @nest & > .bar {
        color: blue;
    }
}
/* 等同于 */
.foo { 
    color: red; 
}
.foo > .bar { 
    color: blue; 
}

.foo {
    color: red;
    @nest .parent & {
        color: blue;
    }
}
/* 等同于 */
.foo { 
    color: red; 
}
.parent .foo { 
    color: blue; 
}

.foo {
    color: red;
    @nest :not(&) {
        color: blue;
    }
}
/* 等同于 */
.foo { 
    color: red; 
}
:not(.foo) { 
    color: blue; 
}

但像下面这样嵌套则是无效的:

/* 无效,因为没有嵌套选择器 */
.foo {
    color: red;
    @nest .bar {
        color: blue;
    }
}

/* 无效,因为不是列表中的所有选择器都包含嵌套选择器 */
.foo {
    color: red;
    @nest & .bar, .baz {
        color: blue;
    }
}

注意,如果使用@nest时记得要和&结合在一起使用才有效。

新增相对单位:caplhrlhvivb

年初和大家在 图解CSS系列 中的《CSS 的值和单位》一文中和大家一起聊了一下CSS中单位和值相关的话题

但在 相对单位 中并没有提到caplhrlhvivb这几个相对单位。

单位名称 描述
cap 元素字体的cap height,即大写字母的标称高度
lh 元素的行高(line-height
rlh 根元素(root)的行高(line-height
vi 等于根元素行内轴方向上初始包含块大小的1%
vb 等于根元素块轴方向上初始包含块大小的1%

从上表的描述来看,其中caplhrlh的计算都和元素的字体以及行高等有关系。我用下图来描述一个字体的Cap Height,Line Height等:

Color Level 4 和 Level 5

CSS Color Level 4Level 5 两个模块主要是为我们推出了一些颜色使用的新属性,比如:

  • <hwb()> :HWB(白色-白色-黑色的缩写)是另一种指定颜色的方法,类似于HSL,它描述了一开始的色调,然后是一定程度的白色和黑色混合到基本色调
  • <lab()><lch()> :Lab是由一个亮度通道和两个颜色通道组成的。在Lab颜色空间中,每个颜色用L(亮度)、a(从绿色到红色的分量)和b(从蓝色到黄色的分量)三个数字表示。而Lch分别表示了颜色的亮度、饱和度和色调
  • <gray()> :灰色是完全去饱和的颜色,gray()函数表示法简化了对这组常见颜色的指定,因此只需要一个数值参数,用来指定颜色的灰度
  • <color()> :该函数允许在特定的颜色空间中指定颜色
  • <device-cmyk()> :该函数是以CMYK(青色、品红、黄色和黑色)组合,在该设备上生成特定的颜色
  • <system-color> :根据用户操作系统来匹配颜色
  • color-mix() :该函数接受两个<color>规范,并在给定的颜色空间中以指定的数量返回它们混合的结果
  • color-contrast() :该函数首先使用一种颜色(通常是背景色),然后使用两种或两种以上颜色的列表,它从该列表中选择亮度对比度最高的颜色到单一颜色
  • color-adjust() :该函数接受一个<color>规范,并通过指定的转换函数在给定的颜色空间中返回调整该颜色的结果 颜色扩展:根据现有的颜色(在这称为“原始颜色”)在函数的目标颜色空间中生成颜色,它是<rgb()><rgba()><hsl()><hsla()><hwb()><lab()><lch()>的扩展颜色

有关于这方面更为详细的介绍可以阅读《图解CSS: CSS 颜色》一文。

对于Web开发者来说,最大的感受是语法规则有较大的变化:

颜色函数 老语法 新语法(不带透明通道) 新语法(带透明通道)
rgb() rgb(0, 0, 0) rgb(0 0 0) rgb(0 0 0 / .5)
rgba() rgba(0, 0, 0, .5) rgba(0 0 0 / .5)  
hsl() hsl(0, 30%, 50%) hsl(0 30% 50%) hsl(0 30% 50% / 50%)
hsla() hsla(0, 30%, 50%, .5) hsla(0 30% 50% / .5)  

来看两个示例:

// Color Level 4
.colour {
    --fn-notation: hsl(2rad 50% 50% / 80%);
    --neon-pink: color(display-p3 1 0 1);
    --gray: lch(50% 0 0);
    --fallback: color(lab 25 25 25, display-p3 1 0 1, #ccc);
}

// Color Level 5
.colour {
    --pink: color-mix(red, white);
    --halfpink: color(var(--pink) / 50%);
    --halfred: rgb(from #f00 / 50%);
    --darkred: hsl(from red h s calc(l * .25));
}

这里再特意提一下display-p3颜色,我们可以配合CSS的媒体查询@media (dynamic-range: high)一起使用:

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

    .neon-pink {
        --neon-glow: color(display-p3 1 0 1);
    }

    .neon-purple {
        --neon-glow: color(display-p3 0 0 1);
    }

    .neon-blue {
        --neon-glow: color(display-p3 0 1 1);
    }

    .neon-green {
        --neon-glow: color(display-p3 0 1 0);
    }

    .neon-yellow {
        --neon-glow: color(display-p3 1 1 0);
    }

    .neon-white {
        --neon-glow: color(display-p3 1 1 1);
    }
}

注意,Display-P3颜色空间颜色要比sRGB颜色空间中的颜色更鲜艳:

也可以说,Display-P3sRGB的一个超集,大约要大35%

Safari 97预览版本可以查看到display-p3的效果:

同样的,在color()函数中使用display-p3指定颜色空间时,也可以和sRGB颜色空间相互转换,如下图所示:

下面是@Adam Argyle 在Codepen提供的一个有关于display-p3的示例:

有关于Web颜色更多的内容还可以阅读:

::marker

::marker也是CSS的伪元素,现在被纳入到CSS Lists Module Level 3规范中。在该规范中涵盖了列表和计算数器相关的属性,比如我们熟悉的list-style-typelist-style-positionlist-stylelist-itemcounter-incrementcounter-resetcounter()counters()等属性。

在CSS中display设置list-item值之后就会生成一个Markers标记以及控制标记位置和样式的几个属性,而且还定义了计数器(计数器是一种特殊的数值对象),而且该计数器通常用于生成标记(Markers)的默认内容。

一时之间,估计大家对于Markers标记并不熟悉,但对于一个列表所涉及到的相关属性应该较为熟悉,对于一个CSS List,它可以涵盖了下图所涉及到的相关属性:

事实上,CSS的::marker和伪元素::before(或::after)类似,也可以通过contentattr()一起来控制Marker标记的效果。需要记住,生成个性化Marker标记内容需要做到几下几点:

  • 非列表项li元素需要显式的设置display:list-item (内联列表项需要使用display: inline list-item
  • 需要显式设置list-style-typenone
  • 使用content添加内容(也可以通过attr()配合data-*来添加内容)

比如下面这个小示例:

另外,::marker还没有得到浏览器支持之前,一般都是使用CSS的计数器来实现一些带有个性化的有顺序列表,比如下面这样的效果:

是不是很有意思,有关于::marker伪元素更详细的介绍,还可以阅读:

:focus-visible:focus-within

一直以来我很容易把:focus-within:focus-visible混淆。其实:focus-within:focus-visible都是CSS选择器 Level 4中用户操作类伪类选择器。早前在《初探CSS 选择器Level 4》中聊过:focus-within,但没有聊过:focus-visible

另外,在《CSS :focus-within》教程中就提到过, :focus-within能非常方便处理获取焦点状态。当元素本身或其后代元素获得焦点时,:focus-within伪类的元素就会有效:focus-within伪类选择器的行为本质上是一种父选择器行为,子元素的状态会影响父元素的样式。由于这种“父选择器”行为需要借助用户的行为触发,属于“后渲染”,不会与现有的渲染机制相互冲突。

如果上面的介绍让你感到困惑的话,可以看下面这个Demo:

你会发现,当<form>的后代元素<input>得到焦点时,<form>会有一个放大的效果:

实现上图的效果代码非常的简单:

form:focus-within {
    box-shadow: 0px 0.2em 2.5em #c4c4c4;
    transform: scale(1.025); 
}

对于:focus-visible伪类来说,当元素匹配:focus伪类并且客户端(UA)的启发式引擎决定焦点应当可见时就会生效。这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

简单点说,按键盘tab键和鼠标点击得到的焦点效果不同。比如:

/* 链接得到焦点时的样式 */ 
a:focus { 

}

/* 
* 1. 如果链接有焦点,但是浏览器通常不会显示默认的焦点样式,会覆盖上面的焦点样式 
* 2. 不是按键盘`tab`键让链接得到的焦点,比如说鼠标点击链接
*/
a:focus:not(:focus-visible) {

}

/* 按键盘tab键让链接得到焦点的样式 */ 
a:focus-visible { 

}

来看一个具体的案例:

在上面的示例中分别用鼠标点击链链和按键盘tab键让链接得到焦点,它的样式是不一样的:

CSS逻辑属性

如果你阅读过《Web中向左向右》一文或者有接触过CSS书写模式特性,你会发现以前我们熟悉的物理属性在不同的语言环境之下很难满足布局的需求,比如英语和阿拉伯语,日语和蒙语等,我们设置的margin-left有可能不是margin-leftwidth也有可能不是width

这个时候,CSS逻辑属性就显得尤其重要。换句话说,逻辑属性的出现,我们以往熟悉的盒模型也将带来很大的变化

我在《CSS逻辑属性》一文中最末尾提供了我们熟悉的 物理属性逻辑属性 的对应关应:

对于块轴(block axis)和内联轴(inline axis)我在《CSS逻辑属性》一文中提到过,同样用一张图来描述这两者吧:

块轴和内联轴和CSS的书写模式writing-mode以及direction和HTML的dir有关系。换句话说:

  • 块轴:主要定义网站文档(元素块)流,CSS的书写模式writing-mode会影响块轴的方向
  • 内联轴:主要定义网站的文本流方向,也就是文本的阅读方式,CSS的direction或HTML的dir会影响内联轴的方向

有关于writing-modedirectiondir对Web排版的影响相关的详细介绍可以阅读《Web中向左向右》一文。

数据服务

数据服务指的是 Data Saver。啥意思呢?不做解释,直接用一段代码来描述:

@media (prefers-reduced-data: reduce) {
    header {
        background-image: url(/grunge.avif);
    }
}

我想大家对于@media (prefers-reduced-data: reduce)应该不会陌生吧。是的,它就是我们所说的CSS媒体查询。只不过稍有不同的是,这个媒体查询是根据用户在设备上的设置喜好来做条件判断。比如上面示例代码,当用户在设备上开启了“Low Data Mode”(低数据模式),会加载grunge.avif图像,可以帮助iPhone上的应用程序减少网络数据的使用:

到目前为止,CSS媒体查询提供了多个媒体特性,可以以用户在设备上的喜好设置做为判断,比如iOS13+开始,iPhone提供的DarkMode模式(prefers-color-scheme

比如,使用prefers-reduced-motion媒体查询用于检测用户的系统是否被开启了动画减弱功能:

上面提到的这些媒体查询条件都是在 CSS Media Queries Level 5 模块中新增的。

除了上面提到的之外,还有一些我们平时很少见的媒体查询条件,比如:

@media (hover: hoveer) {}

@media (hover: none) and (pointer: coarse) {}

@media (hover: none) and (pointer: fine) {}

@media print and (min-resolution: 300dpi) {}

@media (scan: interlace) {}

@media (update) {}

@media(environment-blending: additive){}

@media (color) {}

如果你对媒体查询新增的以用户在设备喜好设置做为判断条件相关的知识感兴趣的话,可以花点时间阅读《CSS媒体查询新特性》一文。

在@argyleink大神分享的PPT中,除了新特性的介绍之外,还介绍了一些工具,但这里不做这方面的整理。感兴趣的同学可以阅读PPT。但接下来,我继续以我自己的角度继续和大家分享一些CSS的新特性。

接下来的一些CSS特性,可能对于很多同学而言应该是新的,但对于我而言有点不新!不过并不影响我继续向大家介绍。

containcontent-visibility

这是前两天在《初探CSS的容器模块》文中刚整理的CSS特性。这两个属性是属于 CSS容器模块 的,其最大的特点应该是可以帮助Web开发者提高Web页面的性能:

当容器的内容发生变化时,浏览器考虑到其他元素可能也会发生变化,于是就会去检查页面中所有的元素。一直以来浏览器都是这么做的,大家都习以为常了。但从另一方面来说,开发者很清楚当前修改的元素是否独立、是否影响其他元素。因此如果开发者能把这个信息通过CSS告诉浏览器,那么浏览器就不需要再去考虑其他元素了,这就是非常完美的事情。而CSS容器模块中的contain属性就为我们提供了这种能力。

CSS容器模块中的content-visibility属性会显著影响第一次下载和第一次渲染的速度。此外,你可以立即与新渲染的内容交互,而无需等待内容的其余部分加载。该属性强制用户代理跳过不在屏幕上的标记和绘制元素。实际上,它的工作方式类似于延迟加载,只是不加载资源,而是渲染资源

可用于双屏幕和可折叠屏幕的媒体查询条件和环境变量

随着技术不断的发展,我们所面对的终端个性化越来越强,比如现在市场上已有或将有的双屏幕和可折叠屏幕设备:

作为Web开发者,我们终究有一天需要面对这些终端的适配处理。到目前为止,CSS世界具备处理方面适配的能力,即使用screen-spanning媒体查询条件和env(fold-left)env(fold-top)env(fold-height)env(fold-width)环境变量:

有了这些特性,我们就可以很轻易的实现像下图这样的布局效果:

如果你对这方面的知识感兴趣的话,可以阅读下面几篇文章,这几篇文章也是全网介绍双屏幕和可折叠屏幕最详细的教程:

@property

CSS Houdini中,最令人兴奋的是给CSS自定义属性和值的API。这个API通过赋予CSS自定义属性(通常也称为CSS变量)语义意义(由语法定义)甚至回退值来增强CSS自定义属性。

简单地说,可以使用CSS Houdini的CSS自定义属性和值CSS.registerProperty()来注册一个自定义属性:

CSS.registerProperty({
    name: '--colorPrimary',
    syntax: '<color>',
    initialValue: 'magenta',
    inherits: false
});

这样一来就可以使用已注册好的--colorPrimary自定义属性:

.card {
    background-color: var(--colorPrimary); /* magenta */
}

.highlight-card {
    --colorPrimary: yellow;
    background-color: var(--colorPrimary); /* yellow */
}

.another-card {
    --colorPrimary: 23;
    background-color: var(--colorPrimary); /* magenta */
}

现在或者将来,我们可以直接使用CSS的@property来注册一个自定义属性:

@property --gradient-start {
    syntax: "<color>";
    initial-value: white;
    inherits: false;
}

在CSS中就可以直接像下面这样使用:

.el {
    --gradient-start: white;
    background: linear-gradient(var(--gradient-start), black);
    transition: --gradient-start 1s;
}

.el:hover {
    --gradient-start: red;
}

比如下面这个示例(请使用Chrome 85+查看):

在CSS的世界中,还有另外一套规范是和CSS自定义属性有关的,那就是 CSS Custom Properties for Cascading Variables Module Level 1。使用--在选择器块中声明自定义属性,然后使用var()函数引用已声明的自定义属性,将其当作CSS属性的值:

:root {
    --color: #f09
}

body {
    color: var(--color)
}

到目前为止,CSS自定义属性(也有同学称为CSS变量)已经得到了主流浏览器的使用,而且在一些大型Web应用中可以看到其身影。另外CSS自定义属性被运用的场景也很多,比如说@Adam Argyle就用CSS自定义属性模拟了一套缓动函数,我们可以用于CSS Animation中:

aspect-ratio

aspect-ratioCSS Box Sizing Module Level 4 模块中的一个用来计算元素盒子宽高比的属性。在这个属性还没有之前,在CSS中都是通过其他一些方法来模拟宽高比的效果。比如:

.aspectration { 
    position: relative;/*因为容器所有子元素需要绝对定位*/ 
    height: 0; /*容器高度是由padding来控制,盒模型原理告诉你一切*/ 
    width: 100%; 
} 

.aspectration[data-ratio="16:9"] { 
    padding-top: 56.25%; 
} 

.aspectration[data-ratio="4:3"]{ 
    padding-top: 75%; 
}

如果浏览器支持了aspect-ratio的话,可以像下面这样使用:

div {
    aspect-ratio: 1 / 1;
}

比如@rachelandrew在Codepen提供的一个示例,我在该示例的基础上稍作调整了一下:

gap

“Gap”从字面上来解释的话可以有“间隙,间隔”之意。那么在Web的布局中总是避免不了处理块与块以及行与行之间的间距。

而在CSS的世界中,用来控制元素之间的间距的间距,一般会使用盒模型中的外距,即 margin 属性,但是往往很多时候,使用margin来控制元素之间间距并不能很好的满足设计师的诉求。比如说,元素只和元素之间有间距,但和它的父容器之间没有任何的间距。针对于这样的场景,使用gap属性会比使用margin要容易控制的多。

注意,上图来自于《Next-generation web styling》一文

CSS的gap属性自身最大的特点就是:gap是相对于流的,这意味着它会根据内容流的方向动态变化。比如说书写模式的改变,gap也会自动改变。

早期在CSS中,gap分很多种,在不同的容器格式中,叫法不同,比如在多列布局(Multi-column Containers)中对应的是column-gap

body {
    column-gap: 35px;
}

但在网格容器(Grid Containers)又被称为grid-row-gapgrid-column-gap

除此之外,它还可以被运用于Flexbox容器(Flexbox Containers),只不过早前,在Flexbox中没有类似flex-row-gapflex-column-gap这样的属性。

需要注意的是,在Flexbox模块中是没有gap属性,但这并不影响我们在Flexbox布局中使用gap属性,这是因为gap统一纳入到了 CSS Box Alignment Module Level 3模块。而且gaprow-gapcolumn-gap的简写属性:

我们现在可以在多列布局,Flexbox布局和网格布局中像下面这样使用gap

// 多列布局
.multi__column {
    gap: 5ch
}

// Flexbox布局
.flexbox {
    display: flex;
    gap: 20px
}

// Grid布局
.grid {
    display: grid;
    gap: 10vh 20%
}

从上面示例代码中我们可以发现,gaprow-gapcolumn-gap的简写属性,而且gap可以接受一个值也可以接受两个值,当gap只有一个值时,表示row-gapcolumn-gap的值相同;当gap有两个值时,其中第一个值是row-gap,第二个值是column-gap

.gap {
    gap: 10px;
}

// 等同于
.gap {
    row-gap: 10px;
    column-gap: 10px
}

.gap {
    gap: 20px 30px;
}

// 等同于
.gap {
    row-gap: 20px;
    column-gap: 30px;
}

特别声明一点,虽然CSS新增了gap属性(row-gapcolumn-gap),但Grid中早期的grid-row-gapgrid-column-gap属性同样可用。

subgrid

CSS Grid布局是Web布局模式中唯一一种二维布局,也是我自己最认可的布局模式(至少到目前为止还没有发现比Grid更强大的)。如果你从未接触过Grid布局的话,你可以把他想象成最初的table布局,因为他们俩之间有很多概念都非常的相似。

随着Web布局技术不断的更新以及浏览器不断的发展,现在使用Grid布局的越来越多,特别是今年以来,Grid和Flexbox布局的占比越来越近:

上面的数据是来自于MDN,更详细的可以阅读MDN Browser Compatibility Report 2020

暂时把你拉回到90年代,那个时候Web的布局主要以table布局为主,在使用table布局的时候也时常会发现表格嵌套表格:

在Grid布局也是相似的,也会碰到网格嵌套网格。

<!-- HTML -->
<div class="grid">
    <div class="item">
        <div class="subitem"></div>
    </div>
</div> 

/* CSS */
.grid {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    grid-template-rows: repeat(4, minmax(100px, auto));
}

.item {
    grid-column: 2 / 7;
    grid-row: 2 /  4;

    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: repeat(3, 80px);
}

.subitem {
    grid-column: 2 / 4;
    grid-row: 1 / 4;
}

网格嵌套在网格中,各自的网格轨道是相互独立的,不过也会引起子网格中元素对齐会存在一些问题。不过在 CSS Grid Layout Module Level 2 模块中新增了 subgrid 属性(Firefox 71开始就支持该属性)。

有了subgrid之后,在嵌套网格的时候,我们就可以在grid-template-columnsgrid-template-rows设置subgrid。这样一来,上面示例的代码我们就可以修改成:

.grid {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    grid-template-rows: repeat(4, minmax(100px, auto));
}

.item {
    grid-column: 2 / 7;
    grid-rows: 2 / 4;

    display: grid;
    grid-template-columns: subgrid;
    grid-template-rows: subgrid;
}

.subitem {
    grid-column: 3 / 6;
    grid-row: 1 / 4;
}

这样一来,子网格就会继承其父网格的网格轨道,反过来,在使用任何类型的自动调整(比如,automin-contentmax-content等)时也会影响其维度(尺寸)。

在我们平时的一些UI布局中,subgrid就可以用得上了:

我们一起来看一个subgrid的具体实例:

注意,请使用Firefox 71+查看上面的Demo,看到的效果如下:

subgridgrid一样,是一套复杂的体系,如果要说清楚subgrid的话,可能会要多文章文章才能讲清楚。如果你对subgrid感兴趣的话,还可以阅读下面这几篇文章:

瀑布流布局

瀑布流布局(Masonry Layout)也是Web布局中的典型布局之一:

虽然能使用CSS的多列布局、Flexbox布局和Grid布局等模拟出瀑布流布局效果,但更多的同学还是更喜欢使用一些JavaScript库来实现瀑布流布局,比如 Masoonry

为了能让原生的CSS直接实现瀑布流布局效果,早在2017年社区中就有人提出用原生的CSS实现瀑布流布局效果,不幸的是,直到现在也还只是一个实验性的属性,而且仅在Firefox Nightly浏览器中支持。

.masonry {
    display: grid;
    gap: 20px;
    grid: masonry / repeat(auto-fill, minmax(250px, 1fr));
}

比如下面这个Demo:

为了能在Firefox Nightly浏览器能正常的查看上面Demo的效果,你需要确保开启了相应的功能。如果没有的话,请在Firefox Nightly浏览器地址栏中输入about:config,然后搜索layout.css.grid-template-masonry-value.enabled,并将其设置为true

然后重启浏览器,查看Demo,你看到的效果将会是像下面这样:

text-emphasis

先上张图:

上图的效果就是使用CSS的text-emphasis实现的。在以往我们要给文本添加一些装饰效果,除了加粗(font-weight)、倾斜(font-style)、阴影(text-shadow),文本上面或下面添加线条(text-decoration)等之外也没有别的了(当然,还可以使用其他的CSS实现一些特殊效果)。但要实现上图的效果还是有一定难度的。不过text-emphasis的出现,这一切变得要简单地多。

text-emphasis是属于 CSS Text Decoration Module 规范中的一个特性,在 Level 3中和text-emphasis有关的属性还有text-emphasis-styletext-emphasis-color,而且text-emphasis是这两个属性的简写属性。另外还有一个用来指定标记符位置的属性text-emphasis-position

.emphasis {
    text-emphasis: triangle rebeccapurple;
    text-emphasis-position: under;
}

Level 4的规范中还新增了text-emphasis-skip属性

具体的效果如下:

min()max()clamp()

min()max()clamp()三个函数称为“比较函数”。早在《聊聊min()max()clamp()函数》一文中对其做过详细的介绍。这里不做详细介绍,仅和三张图来分别展示他们的功能。

我们可以使用 min()设置最大值

max()min()相反,返回的是最大值。使用max()设置一个最小值:

clamp()函数和min()以及max()不同,它返回的是一个区间值。clamp()函数接受三个参数,即 clamp(MIN, VAL, MAX),其中MIN表示最小值,VAL表示首选值,MAX表示最大值。它们之间:

  • 如果VALMINMAX之间,则使用VAL作为函数的返回值;
  • 如果VAL大于MAX,则使用MAX作为函数的返回值;
  • 如果VAL小于MIN,则使用MIN作为函数的返回值

尝试着拖动浏览器视窗的大小,你可以看到类似下图这样的效果:

变量字体

变量字体是一个非常有意思的CSS特性,它也常被称为“可变字体”,先给大家展示一个Demo:

变量字体的目标是让网站性能更好,同时给用户提供了更多选择和扩展。变量字体是类似矢量图形,允许为各种字体轴定义不同的值。变量字体设计中一般有五个注册轴,包括字体、字宽、斜体和光学尺寸。每个注册轴都有一个对应的四个字母的标记,可以映射到现有的CSS属性:

变量字体注册轴 字母标记 对应的CSS属性
Weight wght font-width
Width wdth font-stretch
Italic ital font-style
Slant slnt font-style
Optical Size opsz font-optical-sizing

除了注册轴之外,字体设计器还可以包含自定义轴。自定义轴让可变字体变得更具创造性,因为不限制自定义轴的范围、定义或数量。与注册轴类似,自定义轴具有相应的四个字母标记。但是,自定义轴的字母标记必须是大写的。例如,你定义了一个注册轴是grade,其对应的字母标记是 GRAD

比如上面示例效果对应的代码:

.text {
    font-weight: 800;
    font-style: italic;
    font-variation-settings: "SSTR" 183, "INLN" 648, "TSHR" 460, "TRSB" 312, "TWRM" 638, "SINL" 557, "TOIL" 333, "TINL" 526, "WORM" 523;
    transition: font-variation-settings .28s ease;
}  

.text:hover {
    font-weight: 400;
    font-style: normal;
    font-variation-settings: "SSTR" 283, "INLN" 248, "TSHR" 160, "TRSB" 112, "TWRM" 338, "SINL" 257, "TOIL" 133, "TINL" 426, "WORM" 223;
}

在Firefox浏览器中,我们还可以通过开发者工具中“字体”选项提供的相关可变字体注册轴的值调整:

调整完之后,可以获得新代码:

p {
    font-size: 60px;
    line-height: 37px;
    letter-spacing: 0.113em;
    font-variation-settings: "SSTR" 450, "INLN" 741, "TSHR" 292, "TRSB" 497, "TWRM" 173, "SINL" 557, "TOIL" 728, "TINL" 526, "WORM" 523, "TFLR" 362, "TRND" 516, "SWRM" 536, "TSLB" 509;
    font-weight: 491;
}

对应效果如下:

如果你对可变(变量)字体相关知识感兴趣的话,可以阅读《图解CSS: 变量字体》一文。

color-scheme

color-scheme是我要向大家介绍的最后一个CSS特性,该属性来自于 CSS Color Adjustment Module Level 1。如果你在自己的项目中实现过iOS的DarkMode的效果,你肯定使用过CSS的媒体查询prefers-color-scheme

:root {
    --color: #fff;
    --color-bg: #000;
}

@media (prefers-color-scheme: dark) {
    --color: #000;
    --color-bg: #fff;
}

body {
    color: var(--color);
    background-color: var(--color-bg)
}

虽然说,这可以让用户根据自己喜好来选择自己喜欢的皮肤,但这并不能覆盖所有的。这个时候可以使用color-scheme属性来做一定的补充。该属性允许用户通过用户代理控制自动调整色彩模式,比如暗色模式,对比度调整或特定所需的配色方案。这些值与用户的首选项进行协商,从而产生影响用户界面(UI)内容的所选颜色方案,例如表单控制和滚动条的默认颜色,以及CSS系统颜色的使用值。

color-scheme有两种用法,先来看第一种:

:root {
    color-scheme: dark light;
}

:root元素上,使用color-scheme颜色方案进行渲染,会影响画布的表面颜色(即全局背景颜色),color属性的初始值和系统颜色的使用值,还应该影响视窗滚动条颜色。

另外一种使用方式是在<meta>标签上:

<meta name="color-scheme" content="dark light" />

要遵守color-scheme CSS属性,需要先下载CSS(如果它是通过<link rel="stylesheet">引用的)并进行解析。为了帮助用户代理立即用所需的颜色方案渲染页面背景,还可以在<meta name="color-scheme">元素中提供一个颜色方案值。

由于meta标记和CSS属性(如果应用到:root)最终都会导致相同的行为,所以我更建议通过meta标记指定颜色方案,这样浏览器可以更快地采用首选方案。

最后给大家提供@tomayac提供的一个关于color-scheme的Demo,下图是darklight下相应的效果:

这里仅仅是简单的说了一下color-scheme属性,如果想深入的了解还是需要阅读一些相关的教程:

小结

这篇文章是阅读@argyleink在2020第四届伦敦CSS分享会上分享的PPT有感而出的。在文章中整理了@argyleink在PPT中提到的一些CSS新特性,另外自己从自己的角度额外增加了一些新的特性,差不多有近24个。当然,上面罗列的一些特性对于有些同学而言不是新特性(对我而言有些也不是新特性),但还是非常值得拿出来介绍,因为这些特性已经或将来不久就可以运用到自己的项目中。

由于要聊的特性个数较多,而且很多特性要聊清楚,聊透彻的话,都需要多篇篇幅才能聊完,因此文章中只做了一个蜻蜓点水,抛砖引玉的作用,如果大家对相应的特性感兴趣的话,可以针对性的去了解。另外,您肯定还有接触过一些新特性,并且没有在文章中提到,如果是的话,欢迎你在评论中与我们一起共享。

最后向大家推荐几篇和这个话题相关的文章:

返回顶部