网页设计新玩法,CSS范围语法到底能不能搞定动态样式?

3,198字
14–20 分钟
in

想给网页整点动态效果,又不想写复杂的JavaScript?最近CSS悄悄上线了一个叫范围语法的新功能,配合容器样式查询和if()函数,直接就能根据自定义属性或者HTML属性的数值变化自动切换样式。这篇文章就手把手带大伙儿看看这玩意儿到底怎么用,从基础概念到实战案例,全都掰开揉碎了讲清楚。

目录

范围语法是个啥

CSS的范围语法说白了就是一套专门用来比较数值的表达式,能判断某个数值是大于、小于还是等于另一个数值。以前只能在媒体查询里拿它来比较视口宽度,现在好了,容器样式查询和if()函数也支持了这套玩法,连自定义属性里存的值都能拿来比较。这就好比给CSS装上了判断大小的脑子,再也不用眼巴巴等着JavaScript来帮忙。

从自定义属性开始玩

设置变量

先建一个容器,往里塞个自定义属性--lightness,这玩意儿存的是0%到100%之间的数值,用来控制背景色的亮度。背景色用HSL格式来写,hsl(270 100% var(--lightness)),色相固定270度,饱和度拉满100%,亮度全靠变量说了算。

#container {
  --lightness: 10%;
  background: hsl(270 100% var(--lightness));
}

写条件判断

现在想让容器里所有文字根据背景亮度自动变色。背景亮的时候就上黑字,背景暗的时候就用白字。这里用@container style()来写条件,判断--lightness这个变量是不是小于50%。如果小于,文字就变白;如果大于等于50%,文字就变黑。

#container * {
  @container style(--lightness < 50%) {
    color: white;
  }
  @container style(--lightness >= 50%) {
    color: black;
  }
}

别放错地方

写这段代码的时候容易犯迷糊,以为把@container直接塞进#container的规则块里就行。真这么干的话,@container查询的对象就变成#container的父容器了,而--lightness这个变量只在#container自己身上定义过,父容器根本没这玩意儿,条件永远不成立。所以必须把@container放在#container的子元素规则里,才能正确读取到父级定义的自定义属性。

if()函数的另一种解法

同一规则块内搞定

容器样式查询得跑到子元素里写,要是嫌麻烦,if()函数能在同一个规则块里直接搞定。逻辑一模一样,就是写法换了个姿势。把条件判断写在color属性里,style(--lightness < 50%)成立就给白字,否则给黑字。

#container {
  --lightness: 10%;
  background: hsl(270 100% var(--lightness));
  color: if(
    style(--lightness < 50%): white;
    style(--lightness >= 50%): black
  );
}

子元素也能继承

if()这玩意儿有个特点,它在子元素里也能直接用,而且继承的是父级定义的自定义属性值。但要是想在子元素里再单独写一套逻辑,就得每个子元素都单独声明一遍color: if(...),代码会变得有点啰嗦。容器样式查询的好处是可以批量处理子元素,各有各的适用场景。

命名容器限定范围

如果想精确控制查询范围,可以给容器起个名字。用container-name属性给#container取个名叫myContainer,然后在子元素的@container里指定这个名字。这样即使页面上有其他容器也定义了相同的自定义属性,查询也只会在指定的容器身上生效,不会串台。

#container {
  container-name: myContainer;
  --lightness: 10%;
  background: hsl(270 100% var(--lightness));
}

#container * {
  @container myContainer style(--lightness < 50%) {
    color: white;
  }
  @container myContainer style(--lightness >= 50%) {
    color: black;
  }
}

从HTML属性里抓数值

消息角标的玩法

很多时候数值不是写在CSS变量里,而是直接写在HTML标签的属性上。比如做个未读消息角标,HTML结构就一个带data-notifs属性的div,属性值存着消息条数。想要角标样式跟着数字变,两位数以内显示圆形的具体数字,超过两位数就显示“99+”并改成胶囊形状。

<div data-notifs="8"></div>

提取数值做判断

attr()函数能把HTML属性的值取出来,但直接取出来默认是字符串,没法跟数字比大小。得在attr()里指定数据类型,写成attr(data-notifs type(<number>)),这样取出来的就是数值。先判断这个数值是不是小于等于99,如果是,就用content属性把data-notifs的原始值显示出来,同时用aspect-ratio: 1 / 1把宽度撑成和高度一样,形成一个圆形的角标。

[data-notifs]::after {
  height: 1.25rem;
  border-radius: 1.25rem;

  @container style(attr(data-notifs type(<number>)) <= 99) {
    content: attr(data-notifs);
    aspect-ratio: 1 / 1;
  }

  @container style(attr(data-notifs type(<number>)) > 99) {
    content: "99+";
    padding-inline: 0.1875rem;
  }
}

数据类型得对齐

写范围判断的时候有个大坑,就是比较的双方数据类型必须一致。attr()取出来的数值是<number>,另一边也得是<number>类型才行。要是直接写99,默认也是数值,这没问题。但如果有一边是带单位的,比如50%,另一边也得是百分比,不能一个用数字一个用百分比。这个规则对所有支持的数据类型都适用,包括<length><percentage><angle><time>这些。

不同单位照样能比

拿字体大小做文章

范围语法对不同单位的数值也能比较,只要它们属于同一数据类型就行。比如1em32px,虽然单位不一样,但都是<length>类型,可以放在一起比大小。搞个<h1>里面套个<span>,给<h1>设置font-size: 31px,那<span>里的1em就相当于31px。用if()判断这个1em是小于32px还是大于32px,来决定font-weight是变细还是变粗。

h1 {
  font-size: 31px;
}

h1 span {
  font-weight: if(
    style(1em < 32px): 100;
    style(1em > 32px): 900
  );
}

默认值带来的小意外

要是没给<h1>设置font-size,那1em就会用浏览器的默认值,一般标题的默认字号是32px。这时候两个条件都不满足,font-weight就用浏览器默认的700。这种写法等于给页面加了个自适应的小开关,开发者一改字号,字体粗细就自动跟着调整,防止出现又小又粗或者又大又细的辣眼效果。

两种方案怎么选

容器样式查询和if()函数都能实现范围判断,但用哪个得看场景。容器样式查询适合批量处理子元素,一个@container规则就能管住所有孩子,而且能通过container-name精确限定作用域。if()函数胜在灵活,可以在同一个规则块里直接声明,省去嵌套的麻烦,适合单独控制某个元素的样式。实际开发的时候,要是控制整个容器内部所有子元素,容器样式查询更省事;要是只想给特定元素加逻辑,if()写起来更顺手。