现代 CSS 你知道多少
写在前面
现代 CSS (Modern CSS)应该是 2023 年前端圈热门话题之一,在最近的 CSS Day 活动上也有这个话题。另外在社区中也不乏现代 CSS 的讨论以及如何使用现代 CSS 特性来编写和组织 CSS 代码,以及如何使用现代 CSS 技术更好的构建出可扩展,未来更友好的 Web 项目。
我在还没有完成《现代 Web 布局》和 《防御式 CSS》小册的时候,就已经准备编写一本有关于 CSS 最新特性方面的小册,我将其命名为《现代 CSS》。
- 《现代 Web 布局》:可用于 Web 布局的 CSS 特性,比如 Flexbox,Grid,多语言布局等;
- 《防御式 CSS》:Web 开发者应该具有“万一”的思想准备,在编写代码的时候就需要考虑其所存在的“风险”,使自己编写出来的代码更健壮
- 《现代 CSS》:CSS 中最新的 CSS 特性理论与实践
开始我们今天的话题,接下来的内容篇幅较长,大约有 10000 字的阅读量!
无论以何种标准衡量,在过去的几年里,CSS 都有很大的进步。它有了更多有用的新特性和改进,其中许多最新的特性都是我们渴望已久的。比如,CSS :has()
选择器、CSS 逻辑属性和逻辑值、视窗动态单位、容器查询单位、容器查询、级联层、相对颜色、混合颜色、CSS 作用域、CSS 嵌套、三角函数、滚动驱动动画、视图过渡和瀑布流布局等。
那么,在项目开发的过程中,我们可以使用现代 CSS 哪些特性?或者说,这些新特性将给我们的 Web 开发带来哪些变化?欲知答案,请继续往下阅读。
现代 CSS 选择器:关系型选择器
虽然 CSS 中已经有很多种不同类型的选择器存在了,但现有的选择器总是不能满足 Web 开发者的需求,例如 Web 开发者也一直期望有一个父选择器,使 Web 开发者能通过子元素选择到父元素。而这一期望,也是随着 CSS 的 :has()
选择器的到来才使得 CSS 中有了所谓的“父选择器”。与此同时,现代 CSS 还新增了不少新的 CSS 选择器,例如 :is()
、:where()
、:not()
、:focus-visible
、:focus-within
、:modal
和 :target
等。
在最新的重置 CSS 样式表中可以看到现代 CSS 选择器的一些身影。比如,我现在在我的重置 CSS 样式表中就会利用现代 CSS 特性,放置一些方便的规则。这些规则的奇妙之处在于,它们也是渐进式增强,并不严格要求回退。如果它们在浏览器中得到支持并被应用,那就太棒了!如果没有,对用户体验没有或只有很小的影响。
例如,我为默认链接设置了一个公用的下划线,其作用域为那些没有类的链接。这是一个假设,无类链接旨在保持常规的下划线外观。更新是将下划线设置为使用相对厚度并增加下划线偏移量。视觉效果可能很小,但它可以提高链接易读性,特别是在列表或其他近距离上下文中呈现时。
/* 链接默认样式 */
a:not([class]) {
/* 相对厚度并增加下划线偏移量 */
text-decoration-thickness: max(0.08em, 1px);
text-underline-offset: 0.15em;
}
max()
函数要求浏览器选择参数列表中较大的那个,这有效地确保了在该规则中,下划线不能比 1px
细。
另一个令人感到兴奋的是,浏览更新了交互元素(可聚焦元素)使用的默认焦点行为的切换:默认为 :focus-visible
。尽管元素元素如何接收焦点都可以使用 :focus
,但 :focus-visible
仅根据用户输入模式产生可见焦点状态。实际上,这意味着通常鼠标用户不会看到链接或按钮等元素的可见焦点(焦点环样式),但通过键盘的 Tab
键访问这些元素的键盘用户将看到可见焦点样式。也就是说,我们的可见焦点样式将只附加到 :focus-visible
状态:
:focus-visible {
--outline-size: max(2px, 0.15em);
outline: var(--outline-width, var(--outline-size)) var(--outline-style, solid) var(--outline-color, currentColor);
outline-offset: var(--outline-offset, var(--outline-size));
}
在这些规则中,CSS 自定义属性用于设置 outline
属性样式。这允许为我们的应用程序的焦点样式创建一个公共的样式,同时允许 Web 开发者根据需要重写组件。
你可能也不太熟悉 outline-offset
属性,它定义了元素和轮廓之间的距离。此属性可以使用负值并将其放置在元素中。我经常对按钮组件样式进行这种重写,以确保轮廓与元素保持可访问的对比。
在我的重置 CSS 样式表中,我还使用了 :where()
与 :not()
选择器组合在一起:
*:where(:not(iframe, canvas, img, svg, video):not(svg *, symbol *)) {
all: unset;
display: revert;
}
主要使用 CSS 的选择器 :where()
来降低选择器权重的。简单地说,带有 :where()
选择器,权重都是 0
。与其相反的是,:is()
选择器可以用来增加选择器权重。
另外,规则中的 all
属性允许你一次性重置多个属性,而 unset
值会执行与 initial
或 inherit
相同的操作。换句话说,unset
关键词可以重置可继承和不可继承的属性。
revert
和 unset
非常像,唯一的区别在于 revert
会把 CSS 属性值重置为 User-agent-stylesheet (浏览器加载的默认 CSS 文件)中对应的值。
除此之外,我重置的最后两项内容涉及改善目标或焦点元素的滚动位置。
使用 scroll-padding
属性,你可以根据元素调整滚动位置。scroll-padding
不会影响 Web 布局,只影响滚动位置的偏移量。
在此规则中,当元素是锚点链接(也称为“文档片段”,即 URI)的目标时,将匹配 :target
选择器。scroll-padding-bock-start
将允许目标元素和视窗顶部之间的空间是 2rem
:
:target {
scroll-padding-block-start: 2rem;
}
在下一条规则中使用 scroll-padding-block-end
允许在焦点元素和视窗底部之间留出空间,这有助于跟踪可见焦点位置:
:focus{
scroll-padding-block-end: 8vh;
}
可以调整这两个规则的值,使其最适合你的应用程序布局。
:has()
选择器虽然是 Web 开发者最为期待的选择器之一,它的出现除了使得 Web 开发者能更好地选中目标元素之外,在部分场景之下可以替代 JavaScript 脚本,实现可动态交互的组件。拿 :has()
选择器为例,它和表单状态元素的结合,就可以改变按钮状态。
form .button {
--button-color: hsl(0, 0%, 90%);
--button-text-color: hsl(0, 0%, 50%);
cursor: not-allowed;
}
form:has(input[type="email"]:valid):has(input[type="checkbox"]:checked) .button {
--button-color: var(--color-primary);
--button-text-color: rgb(0, 25, 80);
cursor: pointer;
}
form:has(input[type="email"]):has(input[type="checkbox"]:checked) .button:hover {
--button-text-color: rgb(0, 25, 80);
--button-color: #2eec96e3;
}
但它却还没有得到 Firefox 的支持。如果你现在就要使用 :has()
选择器的话,你就需要一些降级处理。不过,其特性是很优秀的,在未来,它也将会改变我们编写 CSS 的方式,因为基于它所包含的子元素来选择父元素总是一件好事,对吧!
CSS 逻辑属性和逻辑值
众所周知,现在市场上很多 Web 应用或网站都是支持多语的。例如 CNN 新闻网站就是一个典型的案例。我们随意打开网站中的一个页面:
上图截自:https://edition.cnn.com/interactive/2022/10/us/black-women-nonbinary-surfers-cec-cnnphotos/
正如你所看到的,在桌面端(大屏幕),使用 CSS 将内容区域限制在中间的某个地方。这也意味着容器水平居中。稍微了解 CSS 的同学都知道,在 CSS 中有很多方法可以做到这一点,但这里有一个常见的方法:
header {
max-width: 64rem;
margin-left: auto;
margin-right: auto;
height: 100vh;
}
其实,我们还可以用另一个版本做同样的事情,即使用 CSS 中所谓的逻辑属性替换物理属性:
header {
max-inline-size: 64rem;
margin-inline: auto;
block-size: 100dvh;
}
最终结果是一样的。其中我们需要明确的一点是:
inline
等于文本流动的方向block
等于文本流动的垂直方向
对于所有从左到右的语言(LTR),如英语、西班牙语、法语、德语等,以及从右到左的语言(RTL),如阿拉伯语,关键词 inline
指的是水平轴(即 x
轴),关键词 block
指的是垂直轴(即 y
轴),垂直于 inline
关键词所在轴。
我们在这里讨论的是布局,用网站上的矩形来描述,蓝框上的蓝色箭头表示内联方向的布局,红框上的红色箭头表示块方向的布局。
使用 inline
和 block
术语的逻辑属性适用于 CSS 盒模型和布局相关的 CSS 属性。不过,你还需要知道的是 start
和 end
关键词,其中:
start
指的是文本从哪里开始流动,在 LTR 中是向左,在 RTL 中是向右end
指的是文本流动结束位置,在 LTR 中是向右,在 RTL 中是向左
如果你写 CSS 已经有一段时间了,但还没有使用过 CSS 逻辑属性,那么它们可能会让你觉得有点奇怪,不过你可以学习。只不过,我们需要换过一种方式重新审视和学习 CSS 。正如 @Rachel Andrew 所言:
CSS 已经重构到一定程度,要真正解释 CSS 是如何工作的,我们需要改变我们教授和讨论 CSS 语言的方式。我们需要重新审视学习 CSS 是什么。同时,我们也需要抛弃旧观念。只有当我们这样做的时候,我们才不会觉得 CSS 是脆弱的、易碎的、古怪的语言。
重新回到我们的话题中来。使用 CSS 逻辑属性还有一个更大、更重要的原因:构建多语言 Web 应用或网站。比如下面这个示例:
我们继续回到 CNN 的 “我只想冲浪”这个页面,如果使用 CSS 逻辑属性的话,将会获得更多的好处。当前放置文章标题的 CSS 是这样的:
header {
max-width: 540px;
margin: 10px auto 0 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: left;
}
这并没有错。它只是没有使用 CSS 逻辑属性。
如果我们使用谷歌翻译将这个页面翻译成阿位伯语言(即从右向左的语言),其结果是这样的:
翻译之后的阿拉伯文,header
区块依旧位于左下角,并且文本仍然是左对齐。实际上,这并不符合阿拉伯文(RTL)的阅读模式。我们可以使用 CSS 逻辑属性,使其得到更好的结果:
header {
max-inline-size: 540px;
margin-block-start: 10px;
margin-inline-end: auto;
margin-block-end: 0;
margin-inline-start: 0;
position: absolute;
inset-block-end: 0;
inset-inline-start: 0;
inset-inline-end: 0;
text-align: start;
}
如果你足够仔细的话,你可能发现了,图片上的渐变效果并没有同时调整,正确的效果应该是这样:
不幸的是,目前为止在渐变中还没有相关的逻辑属性,比如,还没有像下面这样的逻辑值来根据语言阅读模式自动调整渐变方向:
.gradient {
background-image: linear-gradient(
to inline end,
#000,
transparent 50%
);
}
不过我们可以使用其他的方式来处理:
.gradient {
background-image: linear-gradient(
90deg,
#000,
transparent 50%
);
}
[dir="rtl"] .gradient {
background-image: linear-gradient(
270deg,
#000,
transparent 50%
);
}
CSS 相对单位
CSS 中的相对单位有很多种类型,比如字体相对单位、视窗相对单位等。尤其是视窗相对单位,它在构建全屏布局时非常实用。例如,在 header
中设置了 height
(或 block-size
)的值为 100vh
,使 header
的高度和浏览器视窗的高度一样,达到了设计师想要的效果。
这样做在桌面端一点问题都没有,但在移动端,它就不一定完美了。在手机浏览器上,原本的 vh
视窗单位并不会考虑浏览器工具栏、状态栏和地址栏所占的高度。换句话说,100vh
的高度可能会有不同的情况:
虽然有一些 Hack 手段可以解决这方面的问题,但它不是最优解。
header {
height: 100vh;
}
@supports (-webkit-touch-callout: none) {
header {
height: -webkit-fill-available;
}
}
庆幸的是,CSS 新增的动态视窗单位 dvh
可以很好的帮助我们解决。CSS 的 dvh
单位相当于 svh
和 lvh
:
也就是说,dvh
完全解决了这个问题,并且到处都支持,所以我们基本上可以使用它们。这是最近几年最受欢迎的 CSS 改进之一。
容器查询
容器查询主要分为容器尺寸查询和容器样式查询两种,它们都非常棒。在今天的组件驱动世界中,它们是完全有意义的。这是一件大事,因为我们的行业有一个广泛的共识,那就是 Web 应用或网站应该用 Web 组件来构建。Web 设计师们一致认为,用大大小小的组件构建设计系统是可行的。目前所有的 JavaScript 框架都认为,应该构建组件并将它们组合在一起,以使其最适合你的项目。事实上,Web 开发者也喜欢这种思维模式,大家似乎也理解它。
所以,你有了这些组件,它们的部分意义在于你可以在任何需要的地方使用它们。这些组件并不知道自己会放置页面中的哪个位置,同时 Web 开发者也不知道组件在页面不同位置上会占据多少空间(屏幕空间)。因此,容器查询特性的出现,可以使得 Web 开发者更好的构建 Web 组件,做到一次构建,随地布署。
以容器查询中的尺寸查询为例。首先,你要有一个容器,并且显式使用 container
(或 container-type
) 告诉浏览器,该容器是一个查询容器。如此一来,浏览器就可以根据容器尺寸变化来调整其子元素的样式。
<div class="component--container"><!-- 查询容器 -->
<div class="component"><!-- Web 组件 -->
</div>
</div>
.component--container {
container-type: inline-size;
}
.component {
padding: 10cqmin;
font-size: 5cqi;
}
当你调整容器尺寸时,组件的内距和字号也会随着调整:
这是非常实用的。当然,你可能会说,使用视窗单位 vw
同样可以实现。这样说似乎也是对的,但这只是偶然。你可以看看下面这个示例,视窗尺寸没有做过任何调整,只是 Web 组件放在不同位置,它的容器尺寸也相应会自动调整。你会看到卡片组件的标题字大小会随着组件容器尺寸调整:
对于流体排版这还不是最佳的方案,更好的方案是使用 clamp()
对组件标题大小做一个约束,例如:
.card h3 {
font-size: clamp(1.25rem, 5cqi, 1.5rem);
}
这样一来,卡片组件标题最小不会小于 1.25rem
,最大不会大于 1.5rem
。
上面这两个示例向大家演示了容器查询是如何实现流体排版。其实,它对于 Web 组件布局也是非常实用的,比如下面这个日历示例:
同样,你需要一个实际的“容器”来实现这一点,并且你不能为容器本身设置样式。结果是这样的:
<div class="calendar-wrap">
<div class="calendar">
<!-- 日历组件所需的 HTML 结构 -->
</div>
</div>
.calendar-wrap {
container: Calendar / inline-size;
}
.calendar {
/* 日历所需的样式 */
}
@container Calendar (width < 690px) {
.calendar {
/* 容器尺寸小于 690px, 日历布局所需样式 */
}
}
@container Calendar (width < 360px) {
.calendar {
/* 容器尺寸小于 360px, 日历布局所需样式 */
}
}
我们再来看一个 CSS 容器查询中的样式查询相关的案例。
这是一个典型的多语言版本的 Web 组件,该组件排版和尺寸大小都易于解决,使用前面介绍的逻辑属性和逻辑值即可。但卡片渐变背景的方向是逻辑属性和逻辑值还无法做到的。就此而言,CSS 样式查询就可以很好的解决:
.card {
--bg-angle: to right;
--bg: linear-gradient(var(--bg-angle), #5521c3, #5893eb);
background: var(--bg, lightgrey);
}
.card__container[dir="rtl"] {
--dir: rtl;
direction: var(--dir);
}
@container style(--dir: rtl) {
.card {
--bg-angle: to left;
}
svg {
transform: scaleX(-1);
}
}
请注意,容器查询和样式查询的主要区别在于,容器查询用于查询容器尺寸大小,样式查询用于查询容器样式 。
更为有意思的是,Chromium 团队正在试验一种新的查询类型,称为状态查询。同样拿多语言的 Web 组件为例:
我们知道,在阿拉伯语言(RTL)排版中,箭头图标要做一个镜向处理。要是使用状态查询的话,我们可以这样写 CSS:
@container state(dir: rtl) {
.icon--arrow {
transform: scaleX(-1);
}
}
也就是说,在未来的 CSS 中,容器查询将包含尺寸查询、样式查询和状态查询三种类型:
级联层(@layer)
在 CSS 中,权重一直以来都令 Web 开发者感到困惑和头疼,这也是 Web 开发者不太喜欢 CSS 的主要原因之一。
庆幸的是,随着 CSS 级联层特性的出现,它将颠覆如何解决样式冲突以及什么胜出的想法,这是非常疯狂的。突然间,权重较低的选择器可能会“胜出”,因为它在一个更高的级联层中。
这是一个不争的事实。例如:
body.home nav#main-navigation.top {
background-color: blue;
}
nav {
background-color: white;
}
就上面这两个选择器而言,body.home nav#main-navigation.top
选择器权重要比 nav
高得多:
如果它们都匹配相同的 DOM 元素,那么导航的背景肯定是蓝色的。
在 CSS 级联层(@layer
)还没有出现之前,这样的认知是正确的。换句话说,自 CSS 级联层出现后,body.home nav#main-navigation.top
与 nav
选择器相比,就不一定更高了(胜出)。例如,nav
选择器放在更高有级联层中,它就会胜出:
@layer reset, ds, pages, overrides;
@layer overrides {
nav {
background-color: white;
}
}
@layer pages {
body.home nav#main-navigation.top {
background-color: blue;
}
}
在上面的示例中,在没有第一行声明之下,pages
层将是最高的层,因为它在源顺序中位于后面,但在第一行中,我们声明了我们想要的确切层顺序,因此 overrides
层是最高的层,因此 nav
选择器将获胜。在示例中导航的背景颜色是 white
:
是不是觉得很奇怪!CSS 级联层特性完全逆转 CSS 中的重要工作方式,这使得高层中的低权重选择器胜于低层中的高权重选择器。
从示例结果来看,CSS 级联层确实增加了 CSS 的特性,也使得 Web 开发者有更多的东西需要学习和了解。甚至说,它会使 CSS 的使用变得更加复杂,但从另一个角度来看,它也是 CSS 的一个解决方案,能帮助 Web 开发者更好的管理冲突和 CSS 权重的问题。
这意味着可以更精细地控制何时应用你的样式。
比如:
/* 层结构 */
/* 最佳实践是将 @layer 放在导入文件的顶部,以便更好的组织层 */
@layer base, theme, layouts, components, utilities;
/* Base */
@import '../styles/base/normalize.css' layer(base);
@import '../styles/base/base.css' layer(base);
@import '../styles/base/theme.css' layer(theme);
/* theme variables */
@import '../styles/base/typography.css' layer(theme);
/* theme typography */
@import '../styles/base/utilities.css' layer(utilities);
/* Layouts */
@import '../styles/components/post.css' layer(layouts);
/ Components */
@import '../styles/components/cards.css' layer(components);
@import '../styles/components/footer.css' layer(components);
Demo 地址:https://codepen.io/web-dot-dev/project/editor/ZGQLkq
CSS 中的新颜色
Web 平台上的另一个重要新功能是广色域色彩空间。在广色域色彩在 Web 平台上可用之前,你可以用现代设备查看具有生动色彩的照片,但你无法获得与这些生动值相匹配的按钮、文本颜色或背景。
我们展示了一系列的图像,这些图像在广色域和窄色域之间进行了转换,以说明颜色的生动性及其效果。 但现在,Web 平台上有一系列新的颜色空间,包括 REC2020、P3、XYZ、LAB、OKLAB、LCH 和 OKLCH。
同时,你可以立刻在 DevTools 中看到颜色范围如何扩展,那条白色线条代表 sRGB 范围结束的位置,宽色域颜色范围的开始位置。
还有很多可用于颜色的工具!也别错过所有精彩的渐变改进。甚至有一个全新的工具,@Adam Argyle 构建的用于帮助你尝试新的 Web 色彩选择器和渐变构建器,可以在 gradient.style 上试用。
更进一步,与扩展的颜色空间有关的是 color-mix()
函数。该函数支持混合两种颜色值,以根据被混合的颜色通道创建新值。混合颜色的颜色空间会影响结果。使用更感知色彩空间(例如 oklch
)进行工作,将会在比 srgb
更不同的颜色范围内运行。
color-mix(in srgb, blue, white);
color-mix(in srgb-linear, blue, white);
color-mix(in lch, blue, white);
color-mix(in oklch, blue, white);
color-mix(in lab, blue, white);
color-mix(in oklab, blue, white);
color-mix(in xyz, blue, white);
color-mix()
函数提供了一个长期请求的功能:在保留不透明颜色值的同时,添加一些透明度。现在,你可以在创建具有不同不透明度变化的颜色时使用你的品牌颜色变量。其中的一种方式是将颜色与透明混合。当你将品牌颜色混合到 10%
的透明度时,你将获得 90%
不透明的品牌颜色。你可以看到这如何帮助你快速构建颜色系统。
:root {
--colorPrimary: hotpink;
--colorPrimary-a10: color-mix(in srgb, var(--colorPrimary), transparent 90%);
--colorPrimary-a20: color-mix(in srgb, var(--colorPrimary), transparent 80%);
--colorPrimary-a30: color-mix(in srgb, var(--colorPrimary), transparent 70%);
--colorPrimary-a40: color-mix(in srgb, var(--colorPrimary), transparent 60%);
--colorPrimary-a50: color-mix(in srgb, var(--colorPrimary), transparent 50%);
--colorPrimary-a60: color-mix(in srgb, var(--colorPrimary), transparent 40%);
--colorPrimary-a70: color-mix(in srgb, var(--colorPrimary), transparent 30%);
--colorPrimary-a80: color-mix(in srgb, var(--colorPrimary), transparent 20%);
--colorPrimary-a90: color-mix(in srgb, var(--colorPrimary), transparent 10%);
--colorSecondary: lime;
--colorSecondary-a10: color-mix( in srgb, var(--colorSecondary), transparent 90% );
--colorSecondary-a20: color-mix( in srgb, var(--colorSecondary), transparent 80% );
--colorSecondary-a30: color-mix( in srgb, var(--colorSecondary), transparent 70% );
--colorSecondary-a40: color-mix( in srgb, var(--colorSecondary), transparent 60% );
--colorSecondary-a50: color-mix( in srgb, var(--colorSecondary), transparent 50% );
--colorSecondary-a60: color-mix( in srgb, var(--colorSecondary), transparent 40% );
--colorSecondary-a70: color-mix( in srgb, var(--colorSecondary), transparent 30% );
--colorSecondary-a80: color-mix( in srgb, var(--colorSecondary), transparent 20% );
--colorSecondary-a90: color-mix( in srgb, var(--colorSecondary), transparent 10% );
}
CSS 的嵌套
CSS 嵌套是 Sass 中受欢迎的功能之一,也是多年来 CSS 开发者最重要的需求之一,现在它终于加入到了 Web 平台。嵌套允许开发者以更简洁的、分组的格式书写 CSS,从而减少冗余。
.card {
}
.card:hover {
}
/* CSS 嵌套可以这样写 */
.card {
&:hover {
}
}
你还可以嵌套媒体查询,这也意味着您可以嵌套容器查询。在下面的示例中,当容器有足够的宽度时,将从纵向布局更改为横向布局:
.card {
display: grid;
gap: 1rem;
@container (width >= 480px) {
display: flex;
}
}
当容器宽度有超过或等于 480px
时,布局调整为 flex
。浏览器将在满足条件时,display
将应用新的值。
CSS 作用域
CSS 作用域样式允许开发者指定特定样式应用的边界,从而在 CSS 中创建本地命名空间。在此之前,开发者依靠第三方脚本来重命名类,或者采用特定的命名约定来防止样式冲突,但现在你可以使用 @scope
。
在这里,我们将 .title
元素的作用域限定为 .card
。这将防止该标题元素与页面上的任何其他 .title
元素冲突,例如博客文章标题或其他标题。
@scope (.card) {
.title {
font-weight: bold;
}
}
你可以在此实时演示中查看将 @scope
与作用域限制和 @layer
一起使用的情况。
@layer demo, images, cards;
@layer cards {
@scope (.card) to (> header > *, > figure > *, > footer > *) {
:scope {
display: grid;
background: oklch(50% none none / 20%);
border-radius: 10px;
border: 1px solid oklch(50% none none / 20%);
@media (prefers-color-scheme: light) {
background: white;
box-shadow: 0 30px 10px -20px oklch(0% none none / 25%);
}
> header {
display: grid;
gap: .5ch;
padding: 2ch;
}
> article {
max-inline-size: 50ch;
line-height: 1.5;
padding: 2ch 2ch 1ch;
}
> footer {
display: flex;
justify-content: flex-end;
padding: 1ch 2ch;
gap: 1ch;
}
}
}
}
三角函数
CSS 中新增的三角函数是 CSS 数学函数的一部分。这些函数现在在所有现代浏览器中都已稳定,使你能够在 Web 平台上创建更有机的布局。一个很好的例子是这个径向菜单布局,现在可以使用 sin()
和 cos()
函数进行设计和动画制作。 在下面的演示中,点围绕中心点旋转。与其让每个点绕自己的中心旋转然后向外移动,不如在 X
和 Y
轴上转换每个点。在 X
和 Y
轴上的距离是通过考虑 --angle
的 cos()
和 sin()
分别确定的。
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: true;
}
:root {
--angle: 0deg;
}
@keyframes adjust-angle {
to {
--angle: 360deg;
}
}
:root {
animation: adjust-angle linear 20s infinite paused;
}
:root:has(#enable_animation:checked) {
animation-play-state: running;
}
.dot {
--offset-per-child: calc(360deg / (var(--nth-siblings) + 1));
--angle-offset: calc(var(--nth-child) * var(--offset-per-child));
display: block;
width: var(--tracksize);
aspect-ratio: 1;
border-radius: 50%;
background: hsl(var(--angle-offset) 100% 50%);
border: 1px solid #333;
position: absolute;
left: calc(50% - (var(--tracksize) / 2));
top: calc(50% - (var(--tracksize) / 2));
translate: calc(cos((var(--angle) + var(--angle-offset))) * var(--radius)) calc(sin((var(--angle) + var(--angle-offset))) * var(--radius) * -1);
pointer-events: none;
}
滚动驱动动画
滚动驱动动画允许你根据滚动容器的滚动位置控制动画的播放。这意味着当你向上或向下滚动时,动画会前进或后退。此外,通过滚动驱动动画,你还可以根据元素在其滚动容器中的位置来控制动画。这使你可以创建有趣的效果,例如视差背景图像、滚动进度条以及随着视图进入而显示的图像。
该 API 支持一组 JavaScript 类和 CSS 属性,允许你轻松创建声明性的滚动驱动动画。
要通过滚动驱动 CSS 动画,请使用新的 scroll-timeline
、view-timeline
和 animation-timeline
属性。要驱动 JavaScript Web Animations API,请将 ScrollTimeline
或 ViewTimeline
实例作为 timeline
选项传递给 Element.animate()
。
这些新 API 与现有的 Web Animations 和 CSS Animations API 协同工作,这意味着它们可以受益于这些 API 的优势。其中包括能够在主线程以外运行这些动画。是的,你现在可以通过一些额外的代码轻松地驱动滚动,使其在主线程以外流畅运行。有什么不喜欢的呢?!
使用 animation-timeline
构建类似上面的效果:
@keyframes adjust-progressbar {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
#progressbar {
animation: 1s linear forwards adjust-progressbar;
animation-timeline: scroll(block);
}
视图过渡
我们现在使用的手机应用和操作系统中有很多动画效果:
你打开和关闭的每一个应用程序,每个应用程序的每个屏幕、每个菜单,所有的一切都在动,但在 Web 上却不是这样。我们在 Web 上有很好的动画工具,但它们的使用方式不同。我们只是不像手机那样制作动画。我并不是说我们应该这么做。
但是,我们也没有所有需要的工具,在页面加载之间制作动画。直到现在,我们有了视图过渡 API。
视图过渡 API 使单步更改 DOM 变得容易,同时在两个状态之间创建动画过渡。这些可以是视图之间的简单淡出,但你还可以控制页面的各个部分如何过渡。
视图过渡可以用作渐进增强:将更新 DOM 的代码按任何方法进行分解,并使用视图过渡 API 包装它,以便在不支持该功能的浏览器中提供回退。
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data); return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
Web 开发者可以使用 ::view-transition-old()
和 ::view-transition-new()
来控制过渡的外观。
@keyframes slide-from-right {
from {
opacity: 0;
transform: translateX(75px);
}
}
@keyframes slide-to-left {
to {
opacity: 0;
transform: translateX(-75px);
}
}
::view-transition-old(root) {
animation: 350ms both slide-to-left ease;
}
::view-transition-new(root) {
animation: 350ms both slide-from-right ease;
}
正如 @Maxi Ferreira 的这个精彩演示所展示的那样,其他页面交互,例如正在播放的视频,在进行视图过渡时仍然可以继续工作。
瀑布流布局
你知道 Pinterest 吗?它是 Web 上最著名的布局之一,即瀑布流布局。虽然在 CSS 使用多列布局和 CSS Flexbox 可以模拟出瀑布流布局效果,但更多的时候还是依赖于 JavaScript 库来构建瀑布流布局效果。
好消息是,CSS 网格布局新增了原生的瀑布流布局相关特性,你只需要用一个 CSS,就可以实现瀑布流布局效果:
案例:使用现代 CSS 构建按钮组件
接下来,我们将以按钮组件为例,来展示现代 CSS 特性给 Web 开发者带来的变化。
正如上图所示,按钮组件有多种不同的变体:
- 按钮元素(
<button>
) - 链接元素(
<a>
) - 文字加图标的按钮
- 图标加文字的按钮
- 纯图标的按钮
这些按钮组件变体,所对应的 HTML 结构如下:
<!-- button 元素 -->
<button type="button" class="button">Button</button>
<!-- a 元素 -->
<a href="" class="button">Link</a>
<!-- 文字加图标 -->
<button type="button" class="button">
Text + Icon Button
<svg class="button__icon" aria-hidden="true" width="24" height="24">
<use href="#star"></use>
</svg>
</button>
<!-- 图标加文字 -->
<button type="button" class="button">
<svg class="button__icon" aria-hidden="true" width="24" height="24">
<use href="#star"></use>
</svg>
Icon + Text button
</button>
<!-- 纯图标 -->
<button type="button" class="button">
<svg class="button__icon" aria-hidden="true" width="24" height="24">
<use href="#star"></use>
</svg>
<span class="inclusively-hidden">Icon only button</span>
</button>
构建按钮组件,需要一些重置样式。只不过有一些重置属性超出了本文的范围。我们在定制按钮组件时,首先起作用的第一个属性与颜色有关。例如文本颜色和背景颜色。
我们将使用 CSS 的自定义属性来创建按钮的文本颜色(color
)和背景颜色(background-color
)。稍后,我们可以利用 CSS 自定义属性轻松创建按钮组件的变体,包括按钮不同状态下的颜色,如 :hover
或 :disabled
。
@layer components {
.button {
color: var(--button-color, var(--primary));
background-color: var(--button-bg, var(--accent));
}
}
在这个示例中,按钮的 color
和 background-color
都使用的是一个未定义的自定义属性,并且使用第二个值(也是自定义属性)作为未定义属性的回退值。
@layer theme {
:root {
--primary: hsl(265 38% 13%);
--accent: hsl(278 100% 92%);
}
}
注意,我们把组件所需的颜色放置在 theme
的层中,这样我们更易于维护 CSS 代码。即:
@layer reset, theme, components;
@layer theme {
:root {
--primary: hsl(265 38% 13%);
--accent: hsl(278 100% 92%);
}
}
@layer components {
.button {
color: var(--button-color, var(--primary));
background-color: var(--button-bg, var(--accent));
}
}
接下来,使用 :has()
选择给按钮组件创建变体。因为 :has()
选择器可以检测出按钮是否包含了图标:
@layer components {
.button:where(:has(.button__icon)) {
display: flex;
gap: 0.5em;
align-items: center;
}
}
上面代码中,使用 :has()
选择器来查看 .button
是否包含 .button__icon
图标。如果有,设置 display
属性的值为 flex
,并且设置 align-items
的值为 center
,同时使用 gap
设置文本和图标之间的间距。因为使用 :has()
选择器会增加基类选择器的权重,所以我们将 :has()
放在 :where()
选择器中,这样可以将其权重降至为 0
。也就是说,选择器只保留类名(.button
)的权重。
在仅有图标按钮的情况下,我们的 HTML 标记提供了一个带有 .inclusively-hidden
类名的 <span>
元素,这样做是为了让屏幕阅读器能更好的理解是什么按钮。只不过,我们需要使用相关技术手段,在视觉上不呈现该元素中的文本内容。同样的,我们使用:has()
和 :where()
组合,来查询 .button
是否包含 .inclusively-hidden
,如果包含,按钮只显示图标,并且按钮是一个圆形:
@layer components {
.button:where(:has(.inclusively-hidden)) {
border-radius: 50%;
padding: 0.5em;
}
}
接下来,对于没有图标的按钮,我们要设置最小的内联轴尺寸(min-inline-size
),并且让文本居中。我们可以通过组合 :has()
和 :not()
来实现:
@layer components {
.button:where(:not(:has(.button__icon))) {
text-align: center;
min-inline-size: 10ch;
}
}
我们最后一个重要的按钮变体是不只有图标的按钮(文本加图标的按钮和图标加文本的按钮)。因此,我们的选择器将再次组合 :has()
和 :not()
来查询没有 .inclusively-hidden
类的按钮:
@layer components {
.button:where(:not(:has(.inclusively-hidden))) {
padding: var(--button-padding, 0.75em 1em);
border-radius: 0;
}
}
按钮初始状态的外观完成之后,我们南非要处理按钮的另外两个状态的外观,即 :hover
和 :focus-visible
。在这里,我们依旧使用 CSS 自定义属性,而且不需要添加额外的属性。
对于悬停状态(:hover
),我们只需更新颜色:
@layer theme {
:root {
--primary: hsl(265 38% 13%);
--accent: hsl(278 100% 92%);
--accent--alt: hsl(279 100% 97%);
}
}
@layer components {
.button:hover {
--button-bg: var(--accent--alt);
--button-color: var(--primary);
}
}
对于 :focus-visible
,我们只需要重新调整 outline
属性的值:
@layer components {
.button:focus-visible {
--outline-style: dashed;
--outline-offset: -0.35em;
}
}
最终所有 CSS 代码如下:
@layer reset, base, theme, components;
@layer reset {
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
button {
font: inherit;
}
svg {
fill: currentColor;
pointer-events: none;
display: block;
width: 1em;
height: 1em;
}
}
@layer base {
.button {
font-size: 1rem;
text-decoration: none;
font-family: inherit;
cursor: pointer;
align-self: start;
justify-self: start;
border: 2px solid currentColor;
font-weight: 600;
letter-spacing: 0.04em;
transition: background-color 180ms ease-in-out;
}
.button:focus-visible {
outline: var(--outline-width, 1px) var(--outline-style, solid) var(--outline-color, var(--outline));
outline-offset: var(--outline-offset, 1px);
}
.inclusively-hidden {
clip-path: inset(50%);
height: 1px;
width: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
}
}
@layer theme {
:root {
--primary: hsl(265 38% 13%);
--accent: hsl(278 100% 92%);
--accent--alt: hsl(279 100% 97%);
--outline: #351d4a;
}
}
@layer components {
.button {
color: var(--button-color, var(--primary));
background-color: var(--button-bg, var(--accent));
}
.button:where(:has(.button__icon)) {
display: flex;
gap: 0.5em;
align-items: center;
}
.button:where(:has(.inclusively-hidden)) {
border-radius: 50%;
padding: 0.5em;
}
.button:where(:not(:has(.button__icon))) {
text-align: center;
min-inline-size: 10ch;
}
.button:where(:not(:has(.inclusively-hidden))) {
padding: var(--button-padding, 0.75em 1em);
border-radius: 0;
}
.button:hover {
--button-bg: var(--accent--alt);
--button-color: var(--primary);
}
.button:focus-visible {
--outline-style: dashed;
--outline-offset: -0.35em;
}
}
小结
总体来说,现代 CSS 除了可以利用这些新特性解决老的 CSS 问题之外,它(现代 CSS)更加注重灵活性、响应式设计、可访问性和可维护性,可以帮助开发者更加轻松地实现各种复杂的设计需求,并提高网站的用户体验和可维护性。
- 强大的选择器 :现代 CSS 支持多种复杂的选择器,例如
:has()
、:not()
、:is()
和:where()
等伪类函数;:modal
和::backdrop
伪类选择器;:focus-visible
和:focus-within
伪类选择器等,这使得开发者除了可以更精确的选择和控制页面中的元素之外,在部分场景之下可以替代 JavaScript 脚本,实现可动态交互的组件。 - 灵活的布局 :现代 CSS 提供了多种灵活的布局方式,例如 Flexbox 和 Grid 布局,这些布局方式可以帮助开发者更加轻松地实现响应式设计和复杂的页面布局。
- 响应式设计 :现代 CSS 的新特性,比如 CSS 的比较函数、CSS 容器查询、样式查询、CSS 作用域等,可以使得 Web 开发者可以基于 Web 组件驱动方式来开发组件。除了使页面可以根据不同设备屏幕尺寸来适应调整宏观布局之外,还可以使组件根据容器尺寸和样式来调整微观布局,以提高用户体验
- 高清颜色 :现代 CSS 提供了更多支持广色域的 CSS 函数,可以为 Web 元素的颜色提供更高清的色彩,使得 Web 页面不会在高清屏幕下失真。
- 动画效果 :现代 CSS 提供了多种实现动画效果的特性,比如路径动画、动画合成、滚动驱动动画、离散属性过渡和视图过渡等,可以帮助开发者实现各种动态效果,使网页更加生动和有趣。
- 动态单位 :现代 CSS 提供了更多的动态单位,比如动态视窗单位、容器单位,新增相对单位,可以帮助开发者更好的控制元素尺寸
- 逻辑属性 :现代 CSS 提供了一些逻辑属性,可以使 Web 开发者开发的页面布局根据书写模式、阅读模式自动匹配,这为开发一个多语言应用或网站带来极大的灵活性。
- 排版特性 :现代 CSS 新增了彩色字体、可变字体、文本装饰、文本断行、首字下沉等各种新特性,可以帮助开发者更加方便地为文本提供一个更好的排版样式,提高网站的用户体验。
- 可访问性 :现代 CSS 注重可访问性,可以通过使用适当的语义化标签和属性,以及提供良好的键盘导航和屏幕阅读器支持等方式,帮助开发者创建更加易于访问的网站。
- 可维护性 :现代 CSS 提供了多种模块化和组件化的方式,例如CSS 变量、CSS 嵌套、CSS 作用域、CSS 级联层等,可以帮助开发者更加方便地维护和更新网站的样式。
写在最后
我为什么要写一本关于现代 CSS 方面的小册呢?除了现有书籍很少有这些新特性介绍之外,还有几个主要原因:
- 现代 CSS 技术不断发展:随着前端技术的不断发展,现代 CSS 技术也在不断更新和演进,出现了越来越多的新特性和新技术。一本关于现代 CSS 技术的书籍可以帮助开发者更好地了解这些新技术,掌握最新的前端开发技能。
- 现代 CSS 技术应用广泛:现代 CSS 技术已经成为前端开发中不可或缺的一部分,应用广泛。一本关于现代 CSS 技术的书籍可以帮助开发者更好地掌握这些技术,提高自己的开发水平和竞争力。
- 现代 CSS 技术复杂度高:现代 CSS 技术相对于旧的 CSS 技术来说,更加复杂。一本关于现代 CSS 技术的书籍可以帮助开发者更好地理解这些复杂的技术,并提供实际的应用案例和最佳实践。
- 现代 CSS 技术提高开发效率:现代 CSS 技术提供了更多便捷的方式,例如使用 CSS 变量、CSS 嵌套、CSS 作用域等,可以帮助开发者更加快速地实现各种设计需求,提高开发效率。一本关于现代 CSS 技术的书籍可以帮助开发者更好地掌握这些技术,提高开发效率。
综上所述,写一本关于现代 CSS 技术的书籍可以帮助开发者更好地了解和掌握这些技术,提高自己的开发水平和竞争力。
那么,作为一名 Web 开发者,尤其是 Web 前端开发者,你有没有必要学习现代 CSS 相关的技术呢?我的回答是需要的,因为:
- 提高工作效率:掌握现代 CSS 技术可以帮助你更加快速地实现各种复杂的设计需求,从而提高工作效率。
- 提高职业竞争力:现代 CSS 技术是前端开发中必不可少的一部分,掌握这些技术可以提高你的职业竞争力,让你在求职和升职方面更具优势。
- 跟上时代潮流:随着互联网技术的不断发展,现代 CSS 技术也在不断更新和完善,学习这些技术可以让你跟上时代潮流,保持前沿的技术水平。
- 提高用户体验:现代 CSS 技术支持响应式设计和可访问性,可以根据不同的设备和屏幕尺寸来自适应地调整网页布局和样式,以提高用户体验。
- 提高网站可维护性:现代 CSS 技术提供了多种模块化和组件化的方式,可以帮助开发者更加方便地维护和更新网站的样式,从而提高网站的可维护性。
总体来说,学习现代 CSS 相关的技术可以帮助你提高工作效率、职业竞争力和用户体验,跟上时代潮流,并提高网站的可维护性。