在 CSS 中,如果我们想要选中一段连续范围的元素,通常会使用 :nth-child() 组合选择器,例如 :nth-child(n + X):nth-child(-n + Y) 。这种写法可以匹配到 X 到 Y 索引之间的元素,是一种常见且实用的技巧。不过,这种方法也存在一个限制:选择范围是固定的,无法像变量一样在样式中动态调整。
随着一些 现代 CSS 特性 的出现,例如 if() 函数、数学函数以及 sibling-index() 函数,我们可以用另一种思路来实现范围选择。通过简单的数学判断,就可以检测当前元素的索引是否落在指定区间内,并根据结果应用对应样式。更重要的是,当我们把范围参数写成 CSS 变量或封装成函数时,选择范围就可以在运行时动态改变,从而带来更加灵活的样式控制方式。
在这节课中,我们将一步步实现这种基于数学计算的范围选择方法,并将其封装成一个易用的 CSS 函数,让你能够像调用工具一样快速选中任意范围的元素。虽然这些特性目前仍处于实验阶段,但它们展示了 CSS 在未来可能具备的更强大逻辑能力。
传统做法
在 CSS 中,如果我们想选中一段连续范围的元素。传统做法通常依赖 :nth-child() 选择器来选取某一范围内的元素。例如,我们可以通过组合两个 :nth-child() 条件来实现一个区间选择:
/* 选择范围 [3, 8] 内的li元素 */
li:nth-child(n + 3):nth-child(-n + 8) {
background: #03A9F4;
}
这段代码的作用是选中索引位于 3 ~ 8 之间的所有 li 元素,并给它们设置一个蓝色背景:
Demo 地址:codepen.io/airen/full/…
也就是说,我可以像下面这样选中索引位于 X 到 Y 区间内的元素:
/* 选择范围 [X Y] 内的任意元素 */
:nth-child(n + X):nth-child(-n + Y) {
/* CSS... */
}
这种写法本质上利用了 :nth-child() 的数学规则。表达式中的 n 必须是一个非负整数(包括 0)。:nth-child(n + X) 用来保证元素索引大于或等于 X ,而 :nth-child(-n + Y) 则限制元素索引小于或等于 Y 。当两从此条件同时满足时,也就是 X ≤ index ≤ Y,对应的元素就会被选择器匹配到。
通过这种方式,我们就可以在 CSS 中实现一个简单而实用的范围选择。不过,这种方法也有一个明显的限制:范围是写死在选择器中的,无法像变量一样在样式中动态修改。接下来我们将看到,借助一些现代 CSS 特性,可以用另一种方式实现更加灵活的范围选择。
现代方法
如果使用现代 CSS,我们可以用另一种思路实现同样的逻辑。通过 sibling-index() 函数获取当前元素在同级元素中的索引,再结合数学表达式和 sign() 函数来判断该索引是否落在指定范围内。例如:
@property --g {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
ul {
--X: ;
--Y: ;
li {
--g: sign((sibling-index() - var(--X) + 1)*(var(--Y) - sibling-index() + 1));
/* 当 --g 等于 1 时,该元素相当于被 :nth-child(n + X):nth-child(-n + Y) 选中 */
background: if(
style(--g: 1): #03A9F4;
);
}
}
这里的核心是一个数学表达式:
(index - X + 1) * (Y - index + 1)
这个公式的核心目的是用一个简单的数学表达式来判断 元素的索引 index 是否位于区间 [X, Y] 内。它实际上是从传统的 :nth-child(n + X):nth-child(-n + Y) 选择器一步步推导出来的。
首先来看 :nth-child(n + X)。这个表达式可以写成一个简单的等式:
n + X = index
整理后得到:
n = index - X
由于 :nth-child() 中的 n 必须是 非负整数(包括 0 ) ,因此必须满足 n ≥ 0。这就意味着:
index ≥ X
换句话说,这个选择器的作用是确保元素的索引不小于 X。
接下来再看 :nth-child(-n + Y)。同样可以写成:
-n + Y = index
整理后得到:
n = Y - index
因为 n 仍然必须是非负整数,所以必须满足:
index ≤ Y
这个条件保证元素的索引不大于 Y。
当这两个选择器组合在一起时:
:nth-child(n + X):nth-child(-n + Y)
实际上表达的就是一个区间条件:
X ≤ index ≤ Y
也就是说,只有当元素索引位于 X 和 Y 之间时,选择器才会匹配成功。
为了用一个数学表达式来描述这个区间,我们可以把它拆成两个条件:
index - X ≥ 0
Y - index ≥ 0
如果把这两个表达式相乘:
(index - X) * (Y - index)
那么当 index 位于区间 [X, Y] 内时,两项都为正数,因此乘积为正数;而当 index 位于区间外时,其中至少有一项为负数,乘积就会变成负数。因此,通过判断这个乘积的正负号,就可以判断元素是否在指定范围内。
不过在 CSS 中,n 允许等于 0,这意味着区间的边界值 X 和 Y 也需要被包含进去。为了确保边界情况仍然得到正数结果,我们需要在公式中加入 +1,最终得到:
(index - X + 1) * (Y - index + 1)
这样,当 index 等于 X 或 Y 时,表达式依然为正数,从而正确地包含区间边界。接下来再结合 sign() 函数,就可以根据结果的正负返回 1 或 -1,进而在 CSS 中通过 if() 判断是否应用对应样式,实现类似 :nth-child() 的范围选择效果。
我们可以利用这种方法,对之前的示例进行一次改造,使范围判断通过数学计算来实现。例如:
@property --g {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
ul {
--X: 3;
--Y: 8;
--g: sign((sibling-index() - var(--X) + 1) * (var(--Y) - sibling-index() + 1));
/* 当 --g 等于 1 时,该元素相当于被 :nth-child(n + X):nth-child(-n + Y) 选中 */
background: if(style(--g: 1): #03A9F4;);
}
最终结果是一样的:
Demo 地址:codepen.io/airen/full/…
与传统选择器相比,这种方法最大的优势在于灵活性。因为 X 和 Y 是 CSS 变量,所以它们可以在运行时动态修改,例如通过交互、动画或其他样式逻辑实时改变选择范围。而经典的 :nth-child() 选择器是静态的,无法在 CSS 中动态调整参数:
Demo 地址:codepen.io/airen/full/…
简单的小结一下。这种“现代方法”的核心,是利用几个新的 CSS 特性,把原本只能写在选择器里的逻辑,改写成可计算、可变量化的样式逻辑。简单来说,主要用到了以下几个新的 CSS 特性:
-
sibling-index(): 这个函数可以获取元素在同级元素中的索引位置,相当于动态得到:nth-child()中的n。有了这个值,就可以在 CSS 中进行数学计算,而不再依赖选择器本身。详细介绍,请参阅《Web UI:CSS 树计数函数:sibling-count()和sibling-index()》和《Web UI:sibling-index()和sibling-count()实战》 -
数学函数(如
sign()): CSS 现在支持一些数学函数,可以用来进行逻辑判断。在这个例子中,sign()用来判断计算结果的正负,从而确定元素的索引是否落在指定范围内。详细介绍,请参阅《CSS 的三角函数》和《Web UI:你所不知道的三角函数》 -
if()条件函数:if()允许在 CSS 中根据条件应用不同的样式。例如,当计算结果为1时,就应用某个样式,从而实现类似“条件选择”的效果。详细介绍,请参阅《Web UI:CSSif语句与条件逻辑》 -
CSS 自定义属性(CSS 变量): 通过
--X和--Y这样的变量,我们可以动态控制范围,而不需要修改选择器。这也是这种方法比传统:nth-child()更灵活的关键。详细介绍,请参阅《CSS 自定义属性你知道多少》和《CSS 自定义属性可以用来做些什么》 -
@property:@property可以为自定义属性声明类型(例如<integer>),让浏览器能够正确解析和计算变量值,在涉及数学计算时会更可靠。详细介绍,请参阅《CSS 自定义属性:@property》和《Web UI:你需要的是@property》
优雅降级
由于这种现代实现依赖一些尚未广泛支持的 CSS 特性(例如 sibling-index() 和 if()),我们可以使用 **@supports 特性查询**来提供一个更优雅的降级方案:默认使用传统方法,在支持新特性的浏览器中再启用现代写法:
例如可以这样写:
/* 默认方案(fallback):传统 :nth-child() */
li:nth-child(n + 3):nth-child(-n + 8) {
background: #03A9F4;
}
/* 如果浏览器支持 if(),启用现代方案 */
@supports (background: if(style(--a:1): red;)) {
@property --g {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
ul {
--X: 3;
--Y: 8;
li {
--g: sign((sibling-index() - var(--X) + 1) * (var(--Y) - sibling-index() + 1));
background: if(style(--g: 1): #03A9F4;);
}
}
}
这种写法的逻辑是:首先提供一个传统的 :nth-child() 方案作为默认样式,确保在所有浏览器中都能正常工作。随后通过 @supports 检测浏览器是否支持 if() 这样的新 CSS 特性。如果支持,就启用新的范围计算逻辑,并覆盖之前的样式。
这种方式的好处是:
-
旧浏览器:使用稳定的
:nth-child()方案 -
支持新特性的浏览器:自动启用更灵活的现代 CSS 实现
-
代码结构清晰,符合渐进增强的思路
因此,在实践中推荐把这种现代 CSS 技术作为一种增强能力来使用,而不是完全替代传统方法。
封装成函数
为了让代码更易复用,我们还可以把这段逻辑封装成一个 CSS 函数。例如定义一个 --range() 函数来判断某个索引是否在指定区间内:
@function --range(--X:1, --Y:1, --_g <integer>: 0) {
--_g: sign((sibling-index() - var(--X) + 1)*(var(--Y) - sibling-index() + 1));
result: var(--_g);
}
li {
--g: --range(3,8);
background: if(style(--g: 1): #03A9F4;);
}
通过这种方式,范围判断逻辑被封装进 --range() 函数中。使用时只需要传入 起始值和结束值(例如 3 和 8),函数就会返回一个结果值。如果当前元素的索引位于该范围内,返回值为 1;否则为 -1。随后通过 if() 判断返回值,就可以决定是否应用对应样式。
Demo 地址:codepen.io/airen/full/…
这种写法不仅让代码结构更加清晰,也让范围选择变得更灵活、可复用、可动态控制。
总结
这节课主要介绍了一种利用现代 CSS 特性实现范围选择的方法。我们首先回顾了传统的实现方式:通过组合 :nth-child(n + X) 和 :nth-child(-n + Y) 来选中某个索引区间内的元素。这种方法简单可靠,但它的范围是写死在选择器中的,无法在样式中动态调整。
随后,我们进一步探索了另一种思路:将“是否在某个区间内”的判断转化为一个数学问题。通过 sibling-index() 获取元素索引,并结合数学表达式 (index - X + 1) * (Y - index + 1),就可以判断当前元素是否位于 [X, Y] 区间内。再配合 sign() 函数和 if() 条件函数,就能够在 CSS 中实现类似“条件判断”的效果,从而动态控制元素样式。
在此基础上,我们还将逻辑封装成一个 CSS 函数 --range() ,使代码更加简洁、可复用。通过这种方式,范围的起始值和结束值可以写成 CSS 变量,并通过交互(例如 range 滑块)实时修改,从而实现比传统 :nth-child() 更灵活的范围控制。
最后,我们也讨论了浏览器兼容性问题。由于这些特性目前仍处于实验阶段,主要只在 Chrome 中可用,因此需要通过 @supports 提供降级方案,在不支持新特性的浏览器中仍然使用传统的 :nth-child() 实现。这种方式符合 渐进增强的设计理念。
总体来说,这个案例不仅展示了一种新的 CSS 技巧,也体现了现代 CSS 正在逐渐具备更强的计算能力和逻辑表达能力。虽然这些特性还没有完全普及,但它们已经让我们看到:未来的 CSS,可能可以完成越来越多原本需要 JavaScript 才能实现的逻辑。