利用现代 CSS 实现区间选择

0 阅读11分钟

在 CSS 中,如果我们想要选中一段连续范围的元素,通常会使用 :nth-child() 组合选择器,例如 :nth-child(n + X):nth-child(-n + Y) 。这种写法可以匹配到 XY 索引之间的元素,是一种常见且实用的技巧。不过,这种方法也存在一个限制:选择范围是固定的,无法像变量一样在样式中动态调整

随着一些 现代 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/…

也就是说,我可以像下面这样选中索引位于 XY 区间内的元素:

/* 选择范围 [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 - index0

如果把这两个表达式相乘:

(index - X) * (Y - index)

那么当 index 位于区间 [X, Y] 内时,两项都为正数,因此乘积为正数;而当 index 位于区间外时,其中至少有一项为负数,乘积就会变成负数。因此,通过判断这个乘积的正负号,就可以判断元素是否在指定范围内。

不过在 CSS 中,n 允许等于 0,这意味着区间的边界值 XY 也需要被包含进去。为了确保边界情况仍然得到正数结果,我们需要在公式中加入 +1,最终得到:

(index - X + 1) * (Y - index + 1)

这样,当 index 等于 XY 时,表达式依然为正数,从而正确地包含区间边界。接下来再结合 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/…

与传统选择器相比,这种方法最大的优势在于灵活性。因为 XY 是 CSS 变量,所以它们可以在运行时动态修改,例如通过交互、动画或其他样式逻辑实时改变选择范围。而经典的 :nth-child() 选择器是静态的,无法在 CSS 中动态调整参数:

Demo 地址:codepen.io/airen/full/…

简单的小结一下。这种“现代方法”的核心,是利用几个新的 CSS 特性,把原本只能写在选择器里的逻辑,改写成可计算、可变量化的样式逻辑。简单来说,主要用到了以下几个新的 CSS 特性:

优雅降级

由于这种现代实现依赖一些尚未广泛支持的 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() 函数中。使用时只需要传入 起始值和结束值(例如 38),函数就会返回一个结果值。如果当前元素的索引位于该范围内,返回值为 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 才能实现的逻辑。