2020年你不应该错过的CSS新特性
@argyleink在第四次的伦敦(LondonCSS 2020)CSS活动中分享了一个有关于CSS特性相关的话题。看了一下这个主题的PPT,里面有些新东西还是蛮有意思的。基于该PPT,我稍微整理近24个CSS方面的新特性,感兴趣的同学可以继续往下阅读。
接下来的内容我将分为:Web动效 、Web排版 、Web性能 、 Web可访问性 、 Web美化 和 其他等六个部分。
下面聊的CSS新特性当中,有部分还处于实验阶段,如果您感兴趣的话,并不会影响你阅读。你也可以选择你感兴趣的部分阅读。(^_^)
NO.1
伦敦CSS 2020
伦敦CSS 2020(https://www.londoncss.dev/)并不是一次性的活动,到我写这篇文章为止已经是第四届(https://www.londoncss.dev/events/event4)了,每届的话题都不同。在这次活动中主要有三个话题:
@argyleink的WHAT'S NEW WITH CSS?
@Stephanie Eckles的STYLE STAGE AND MODERN CSS:Style Stage堪比现代版的CSS禅意花园(CSS Zen Garden),允许任何级别的CSS从业者重新设计基于相同HTML的样式,同时提高他们地现代CSS特性的熟悉程度
@Cassie Evans的PAINTING WITH SVG:介绍了SVG的绘制相关的特性,而且@Cassie Evans有另一篇有关于SVG的文章,非常值得我们花时间去阅读:Making lil' me - part 1
接下来,我们回到今天的主题:2020年你不应该错过的CSS新特性。
NO.2
Web动效
动态模糊(Motion Blur)
Motion Blur(动态模糊),这个概念也是我第一次接触的。查了一些资料才明白,动态模糊是一种模糊效果,它只在特动移动的时候才会让物体模糊,通常这种模糊是在物体移动的方向上应用的,这就是当你试图在拍一个移动的物体的效果。
当你拍摄一个物体(或是一个人),就像下图,这个模糊就会发生,因为这个物体(人运动)移动的速度超过了相机拍摄所需的曝光时间,所以这个物体会在最终的照片中出现多次,因为它在关键时刻移动。
在我们平时制作动效的时候,很少会考虑(添加)动态模糊效果,另外在CSS和SVG动画中也缺少这方面的效果,因此大多数Web动画看起来很生硬。也就是说,如果我们给动画在运动过程中添加一些模糊效果,会让这种动效更贴近现实生活。
为此,@argyleink向W3C规范提出了有关于动态模糊相关的实现方案(建议)(https://github.com/w3c/csswg-drafts/issues/3837),他为开发者(CSSer)推荐了一种方法,在CSS的animation
是新增了 motion-rendering
和 motion-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);
}
}
来看一个简单的示例(https://codepen.io/airen/pen/oNxQoLw):
这里还是用文档中提供的一个带有动效的图向大家展示动态模糊的效果:
.blur {
animation: blur 250ms;
}
@keyframes blur {
0% {
-webkit-filter: blur(0px);
}
50% {
-webkit-filter: blur(5px);
}
100% {
-webkit-filter: blur(0px);
}
}
另外,现在提供的 motion-rendering
和 motion-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中的滤镜来模拟动态模糊效果:
SVG 1.1: Filter Effects(https://www.w3.org/TR/SVG11/filters.html)
SVG Filters 101(https://tympanus.net/codrops/2019/01/15/svg-filters-101/)
@scroll-timeline = @scroll-timeline <timeline-name> { <declaration-list> }
@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 }
}
}
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)
});
}
再来看一个滚动计时器的效果(https://codepen.io/airen/pen/yJGJEJ):
上面的示例,我们是使用渐变来模拟的一个效果,但有了@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)
}
);
}
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: <a href="https://drafts.csswg.org/scroll-animations-1/">'rotateZ(0)','rotateZ(4turn)']
},{
duration: 1000,
fill: 'both',
timeline: new ScrollTimeline({
scrollSource: counter,
fill: 'both',
}),
})
})
随着Web布局技术不断的更新以及浏览器不断的发展,现在使用Grid布局的越来越多,特别是今年以来,Grid和Flexbox布局的占比越来越近:
暂时把你拉回到90年代,那个时候Web的布局主要以table
布局为主,在使用table
布局的时候也时常会发现表格嵌套表格:
<!-- 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;
}
有了subgrid
之后,在嵌套网格的时候,我们就可以在grid-template-columns
和grid-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;
}
这样一来,子网格就会继承其父网格的网格轨道,反过来,在使用任何类型的自动调整(比如,auto
、min-content
、max-content
等)时也会影响其维度(尺寸)。
我们一起来看一个subgrid
的具体实例(https://codepen.io/antonjb/pen/rNNgxWV)。注意,请使用Firefox 71+查看上面的Demo,看到的效果如下:
subgrid
和grid
一样,是一套复杂的体系,如果要说清楚subgrid
的话,可能会要多文章文章才能讲清楚。如果你对subgrid
感兴趣的话,还可以阅读下面这几篇文章:
CSS Grid Layout Module Level 2:
subgrid
(https://drafts.csswg.org/css-grid-2/#subgrids)Hello
subgrid
!(https://noti.st/rachelandrew/3sescR/hello-subgrid)CSS Grid Level 2: Here Comes Subgrid(https://www.smashingmagazine.com/2018/07/css-grid-2/)
MDN: Subgrid(https://developer.mozilla.org/en-US/docs/Web/CSS/CSSGridlayout/Subgrid)
Irregular-shaped Links with Subgrid(https://css-irl.info/irregular-shaped-links-with-subgrid/)
随着技术不断的发展,我们所面对的终端个性化越来越强,比如现在市场上已有或将有的双屏幕和可折叠屏幕设备:
如果你对这方面的知识感兴趣的话,可以阅读下面几篇文章,这几篇文章也是全网介绍双屏幕和可折叠屏幕最详细的教程:
瀑布流布局(Masonry Layout)也是Web布局中的典型布局之一:
.masonry {
display: grid;
gap: 20px;
grid: masonry / repeat(auto-fill, minmax(250px, 1fr));
}
比如这个Demo(https://codepen.io/rachelandrew/pen/qBOpjPx)。
然后重启浏览器,查看Demo,你看到的效果将会是像下面这样:
“Gap”从字面上来解释的话可以有“间隙,间隔”之意。那么在Web的布局中总是避免不了处理块与块以及行与行之间的间距。
注意,上图来自于《Next-generation web styling(https://web.dev/next-gen-css-2019/)》一文。
CSS的gap
属性自身最大的特点就是:gap
是相对于流的,这意味着它会根据内容流的方向动态变化。比如说书写模式的改变,gap
也会自动改变。
body {
column-gap: 35px;
}
我们现在可以在多列布局,Flexbox布局和网格布局中像下面这样使用gap
:
// 多列布局
.multi__column {
gap: 5ch
}
// Flexbox布局
.flexbox {
display: flex;
gap: 20px
}
// Grid布局
.grid {
display: grid;
gap: 10vh 20%
}
.gap {
gap: 10px;
}
// 等同于
.gap {
row-gap: 10px;
column-gap: 10px
}
.gap {
gap: 20px 30px;
}
// 等同于
.gap {
row-gap: 20px;
column-gap: 30px;
}
特别声明一点,虽然CSS新增了gap
属性(row-gap
、column-gap
),但Grid中早期的grid-row-gap
和grid-column-gap
属性同样可用。
.aspectration {
position: relative;/*因为容器所有子元素需要绝对定位*/
height: 0; /*容器高度是由padding来控制,盒模型原理告诉你一切*/
width: 100%;
}
.aspectration<a href="https://codepen.io/rachelandrew/pen/WNrRZaV">data-ratio="16:9"] {
padding-top: 56.25%;
}
.aspectration[data-ratio="4:3"]{
padding-top: 75%;
}
如果浏览器支持了aspect-ratio
的话,可以像下面这样使用:
div {
aspect-ratio: 1 / 1;
}
比如下面这个手风琴的效果就是用:target
伪元素制作(https://codepen.io/airen/pen/PxYxap)的:
这里简单的来看看:target
和:target-within
的作用。
比如下面这个简单的示例(https://codepen.io/airen/pen/BaKvjGj):
<!-- 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%);
}
这个时候,CSS逻辑属性就显得尤其重要。换句话说,逻辑属性的出现,我们以往熟悉的盒模型也将带来很大的变化:
块轴和内联轴和CSS的书写模式writing-mode
以及direction
和HTML的dir
有关系。换句话说:
max()
和min()
相反,返回的是最大值。使用max()
设置一个最小值:
一直以来,在Web的排版中行高(line-height
)总是令Web开发者感到困惑和头痛,主要是因为line-height
在CSS中是一个非常复杂的体系。他的计算总是会涉及到很多因素:
在还原UI时,文本的行高总是让我们计算元素块之间的间距带来一定的麻烦:
h1 {
leading-trim: both;
text-edge: cap alphabetic;
}
上面的示例首先使用text-edge
来告诉浏览器想要的文本边缘是cap
高度和字母基线(alphabetic baseline)。然后用leading-trim
对文本两边进行修剪。
示例中的两行简单的CSS创建了一个包含文本的干净的文本框(不受line-height
相关的特性影响)。这有助于实现更精确的间距,并创建更好的视觉层次结构。
CSS的text-edge
和leading-trim
分别可接受的值:
text-edge: leading | <a href="https://matthiasott.com/notes/the-thing-with-leading-in-css?ref=heydesigner"> text | cap | ex | ideographic | ideographic-ink ] [ text | alphabetic | ideographic | ideographic-ink ]?
leading-trim: normal | start | end | both
如果你对leading-trim
特性感兴趣的话,除了阅读规范之外,还可以阅读下面几篇文章:
:root::spelling-error {
text-decoration-line: spelling-error;
}
:root::grammar-error {
text-decoration-line: grammar-error;
}
<a href="https://drafts.csswg.org/css-values-4">spellcheck]::spelling-error {
text-decoration: wavy underline var(--neon-red);
}
[grammarcheck]::grammar-error {
text-decoration: wavy underline var(--neon-blue);
}
[CSS中单位和值(https://drafts.csswg.org/css-values-4) 中单位有:
但在 相对单位(https://drafts.csswg.org/css-values-4/#relative-lengths) 中并没有提到cap
、lh
、rlh
、vi
和vb
这几个相对单位。
从上表的描述来看,其中cap
、lh
、rlh
的计算都和元素的字体以及行高等有关系。我用下图来描述一个字体的Cap Height,Line Height等:
这两个属性是属于 CSS容器模块 的,其最大的特点应该是可以帮助Web开发者提高Web页面的性能:
假设一个页面有很多个元素,在这个示例中,我们有10000
个这样的元素:
<div class="item">
<div>Lorem ipsum...</div>
</div>
使用JavaScript的textContent
这个API来动态更改div.item > div
的内容:
const NUM_ITEMS = 10000;
const NUM_REPETITIONS = 10;
function log(text) {
let log = document.getElementById("log");
log.textContent += text;
}
function changeTargetContent() {
log("Change \"targetInner\" content...");
// Force layout.
document.body.offsetLeft;
let start = window.performance.now();
let targetInner = document.getElementById("targetInner");
targetInner.textContent = targetInner.textContent == "Hello World!" ? "BYE" : "Hello World!";
// Force layout.
document.body.offsetLeft;
let end = window.performance.now();
let time = window.performance.now() - start; log(" Time (ms): " + time + "\n");
return time;
}
function setup() {
for (let i = 0; i < NUM_ITEMS; i++) {
let item = document.createElement("div");
item.classList.add("item");
let inner = document.createElement("div");
inner.style.backgroundColor = "#" + Math.random().toString(16).slice(-6);
inner.textContent = "Lorem ipsum...";
item.appendChild(inner);
wrapper.appendChild(item);
}
}
如果不使用contain
,即使更改是在单个元素上,浏览器在布局上的渲染也会花费大量的时间,因为它会遍历整个DOM树(在本例中,DOM树很大,因为它有10000
个DOM元素):
CSS Containment in Chrome 52(https://developers.google.com/web/updates/2016/06/css-containment)
CSS Containment(https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Containment)
Using content-visibility: hidden(https://www.w3.org/TR/css-contain-2/#using-cv-hidden)
Using content-visibility: auto(https://www.w3.org/TR/css-contain-2/#using-cv-auto)
数据服务指的是 Data Saver。啥意思呢?不做解释,直接用一段代码来描述:
@media (prefers-reduced-data: reduce) {
header {
background-image: url(/grunge.avif);
}
}
比如,使用prefers-reduced-motion
媒体查询用于检测用户的系统是否被开启了动画减弱功能:
上面提到的这些媒体查询条件都是在 CSS Media Queries Level 5(https://www.w3.org/TR/mediaqueries-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特性,它也常被称为“可变字体”,先给大家展示一个Demo(https://codepen.io/airen/pen/yLNpQQW):
.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;
}
如果上面的介绍让你感到困惑的话,可以看下面这个Demo(https://codepen.io/ericwbailey/pen/KQOpRM)。你会发现,当