上次,我们了解了几种在 响应式设计中声明和使用 CSS 自定义属性 的方法。在本文中,我们将更深入地了解 CSS 变量以及如何在可重用组件和模块中使用它们。我们将学习如何使我们的变量可选并设置回退值。
例如,我们将构建一个基于 flexbox 的简单网格系统。网格系统在响应式设计中发挥着至关重要的作用。但是,同时构建一个灵活且轻量级的网格系统可能是一项棘手的任务。让我们看看网格系统的常见方法是什么,以及 CSS 自定义属性如何帮助我们构建它们。
文章系列
- 定义变量和断点
- 构建灵活的网格系统 (本文)
一个简单的 CSS 网格系统
让我们从一个 12 列的网格系统开始
.container {
max-width: 960px;
margin: 0 auto;
display: flex;
}
.col-1 { flex-basis: 8.333%; }
.col-2 { flex-basis: 16.666%; }
.col-3 { flex-basis: 25%; }
.col-4 { flex-basis: 33.333%; }
.col-5 { flex-basis: 41.666%; }
.col-6 { flex-basis: 50%; }
/* and so on up to 12... */
查看 CodePen 上的示例
#5 使用 CSS 自定义属性构建响应式特性,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
这里有很多重复和硬编码的值。更不用说一旦我们添加更多断点、偏移类等,将会生成多少更多代码了。
使用 Sass 构建网格系统
为了使我们的网格示例更易读和维护,让我们使用 Sass 预处理我们的 CSS
$columns: 12; // Number of columns in the grid system
.container {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
max-width: 960px;
}
@for $width from 1 through $columns {
.col-#{$width} {
flex-basis: $width / $columns * 100%;
}
}
查看 CodePen 上的示例
#6 使用 CSS 自定义属性构建响应式特性,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
这绝对更容易使用。当我们进一步开发我们的网格并假设想要将其从 12 列更改为 16 列时,我们只需更新一个变量(与数十个类和值相比)。但是……只要我们的 Sass 更短且现在更易于维护,编译后的代码就与第一个示例相同。我们最终仍然会在最终的 CSS 文件中获得大量代码。让我们探索一下如果我们尝试用 CSS 自定义属性替换 Sass 变量会发生什么。
使用 CSS 自定义属性构建网格系统
在我们开始使用 CSS 自定义属性之前,让我们先从一些 HTML 开始。这是我们想要实现的布局

它由三个元素组成:标题、内容部分和侧边栏。让我们为这个视图创建标记,为每个元素提供一个唯一的语义类(header
、content
、sidebar
)和一个 column
类,表示此元素是网格系统的一部分
<div class="container">
<header class="header column">
header
</header>
<main class="content column">
content
</main>
<aside class="sidebar column">
sidebar
</aside>
</div>
我们的网格系统,如前所述,基于 12 列布局。您可以将其想象成覆盖我们内容区域的叠加层

因此,.header
占据所有 12 列,.content
占据 8 列(总宽度的 66.(6)%),.sidebar
占据 4 列(总宽度的 33.(3)%)。在我们的 CSS 中,我们希望能够通过更改单个自定义属性来控制每个部分的宽度
.header {
--width: 12;
}
.content {
--width: 8;
}
.sidebar {
--width: 4;
}
为了使其工作,我们只需要为 .column
类编写一个规则。幸运的是,大部分工作已经完成了!我们可以重用上一章中的 Sass,并将 Sass 变量替换为 CSS 自定义属性
.container {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
max-width: 960px;
}
.column {
--columns: 12; /* Number of columns in the grid system */
--width: 0; /* Default width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
请注意此处两个重要的更改
--columns
变量现在在.column
规则内部声明。原因是此变量不应在此类的作用域之外使用。- 我们在
flex-basis
属性中执行的数学方程式现在包含在一个calc()
函数中。Sass 中编写的数学计算由预处理器编译,不需要额外的语法。另一方面,calc()
允许我们在实时 CSS 中执行数学计算。方程式始终需要包装在calc()
函数中。
在非常基本的层面上,就是这样!我们刚刚使用 CSS 自定义属性构建了一个 12 列网格系统。恭喜!我们可以就此结束并现在愉快地完成本文,但是……我们通常需要一个稍微复杂一点的网格系统。这时事情变得非常有趣。
查看 CodePen 上的示例
#8 使用 CSS 自定义属性构建响应式特性,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
向网格添加断点
大多数情况下,我们需要布局在各种屏幕尺寸上看起来不同。假设在我们的案例中,我们希望布局在大视口(例如桌面)上保持原样,但在较小屏幕(例如移动设备)上使所有三个元素都成为全宽。

因此,在这种情况下,我们希望我们的变量如下所示
.header {
--width-mobile: 12;
}
.content {
--width-mobile: 12;
--width-tablet: 8; /* Tablet and larger */
}
.sidebar {
--width-mobile: 12;
--width-tablet: 4; /* Tablet and larger */
}
.content
和 .sidebar
现在分别包含两个变量。第一个变量(--width-mobile
)是元素默认应占据的列数,第二个变量(--width-tablet
)是元素在大屏幕上应占据的列数。.header
元素不会更改;它始终占据整个宽度。在大屏幕上,标题应简单地继承它在移动设备上的宽度。
现在,让我们更新我们的 .column
类。
CSS 变量和回退
为了使移动版本按预期工作,我们需要更改 .column
类,如下所示
.column {
--columns: 12; /* Number of columns in the grid system */
--width: var(--width-mobile, 0); /* Default width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
基本上,我们将 --width
变量的值替换为 --width-mobile
。请注意,var()
函数现在采用两个参数。第一个是默认值。它表示:“如果在给定作用域中存在 --width-mobile
变量,则将其值分配给 --width
变量。”第二个参数是回退。换句话说:“如果在给定作用域中未声明 --width-mobile
变量,则将此回退值分配给 --width
变量。”我们设置此回退是为了准备某些网格元素没有指定宽度的场景。
例如,我们的 .header
元素有一个声明的 --width-mobile
变量,这意味着 --width
变量将等于它,并且此元素的 flex-basis
属性将计算为 100%
.header {
--width-mobile: 12;
}
.column {
--columns: 12;
--width: var(--width-mobile, 0); /* 12, takes the value of --width-mobile */
flex-basis: calc(var(--width) / var(--columns) * 100%); /* 12 ÷ 12 × 100% = 100% */
}
如果我们从 .header
规则中删除 --width-mobile
变量,则 --width
变量将使用回退值
.header {
/* Nothing here... */
}
.column {
--columns: 12;
--width: var(--width-mobile, 0); /* 0, takes the the fallback value */
flex-basis: calc(var(--width) / var(--columns) * 100%); /* 0 ÷ 12 × 100% = 0% */
}
现在,我们了解了如何为 CSS 自定义属性设置回退,我们可以通过向我们的代码中添加媒体查询来创建断点
.column {
--columns: 12; /* Number of columns in the grid system */
--width: var(--width-mobile, 0); /* Default width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
@media (min-width: 576px) {
.column {
--width: var(--width-tablet); /* Width of the element on tablet and up */
}
}
这完全按预期工作,但仅适用于内容和侧边栏,即对于在其中声明了 --width-mobile
和 --width-tablet
的元素。为什么?
我们创建的媒体查询适用于所有 .column
元素,即使那些在其作用域中没有声明 --width-tablet
变量的元素也是如此。如果我们使用未声明的变量会发生什么?然后,在计算值时间(即用户代理尝试在给定声明的上下文中计算它时),var()
函数中对未声明变量的引用将被视为无效。
理想情况下,在这种情况下,我们希望忽略 --width: var(--width-tablet);
声明,并改用 --width: var(--width-mobile, 0);
的先前声明。但这不是自定义属性的工作方式!实际上,无效的 --width-tablet
变量仍将在 flex-basis
声明中使用。包含无效 var()
函数的属性始终计算为其初始值。因此,由于 flex-basis: calc(var(--width) / var(--columns) * 100%);
包含无效的 var()
函数,因此整个属性将计算为 auto
(flex-basis
的初始值)。
那么我们还能做什么?设置回退!正如我们之前了解到的,包含对未声明变量的引用的 var()
函数计算为其回退值,只要它已指定。因此,在这种情况下,我们只需为 --width-tablet
变量设置一个回退即可
.column {
--columns: 12; /* Number of columns in the grid system */
--width: var(--width-mobile, 0); /* Default width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
@media (min-width: 576px) {
.column {
--width: var(--width-tablet, var(--width-mobile, 0));
}
}
查看 CodePen 上的示例
#9 使用 CSS 自定义属性构建响应式特性,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
这将创建一系列回退值,使 --width
属性在可用时使用 --width-tablet
,如果 --width-tablet
未声明则使用 --width-mobile
,最终,如果两个变量均未声明则使用 0
。此方法允许我们执行多种组合
.section-1 {
/* Flexible on all resolutions */
}
.section-2 {
/* Full-width on mobile, half of the container's width on tablet and up */
--width-mobile: 12;
--width-tablet: 6;
}
.section-3 {
/* Full-width on all resolutions */
--width-mobile: 12;
}
.section-4 {
/* Flexible on mobile, 25% of the container's width on tablet and up */
--width-tablet: 3;
}
这里我们还可以做的一件事是将默认的0
值转换为另一个变量,这样可以避免重复。这会使代码稍微变长,但更容易更新。
.column {
--columns: 12; /* Number of columns in the grid system */
--width-default: 0; /* Default width, makes it flexible */
--width: var(--width-mobile, var(--width-default)); /* Width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
@media (min-width: 576px) {
.column {
--width: var(--width-tablet, var(--width-mobile, var(--width-default)));
}
}
查看 CodePen
#10 使用 CSS 自定义属性构建响应式功能,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
现在,我们拥有了一个功能齐全、灵活的网格!如何添加更多断点呢?
添加更多断点
我们的网格已经非常强大,但我们通常需要不止一个断点。幸运的是,向我们的代码中添加更多断点非常简单。我们只需要重复使用现有的代码,并添加一个变量。
.column {
--columns: 12; /* Number of columns in the grid system */
--width-default: 0; /* Default width, makes it flexible */
--width: var(--width-mobile, var(--width-default)); /* Width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
@media (min-width: 576px) {
.column {
--width: var(--width-tablet, var(--width-mobile, var(--width-default)));
}
}
@media (min-width: 768px) {
.column {
--width: var(--width-desktop, var(--width-tablet, var(--width-mobile, var(--width-default))));
}
}
查看 CodePen
#11 使用 CSS 自定义属性构建响应式功能,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
减少回退链
我们的代码中有一点看起来不太好,那就是每个断点的回退链都越来越长。如果我们想解决这个问题,可以将我们的方法更改为类似这样的方法。
.column {
--columns: 12; /* Number of columns in the grid system */
--width: var(--width-mobile, 0); /* Width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
@media (min-width: 576px) {
.column {
--width-tablet: var(--width-mobile);
--width: var(--width-tablet);
}
}
@media (min-width: 768px) {
.column {
--width-desktop: var(--width-tablet);
--width: var(--width-desktop);
}
}
查看 CodePen
#12 使用 CSS 自定义属性构建响应式功能,作者是 Mikołaj (@mikolajdobrucki)
在 CodePen 上。
这段代码执行完全相同的工作,但方式略有不同。我们没有为每个断点创建完整的回退链,而是将每个变量的值设置为前一个断点的变量作为默认值。
为什么这么复杂?
看起来我们做了很多工作来完成一项相对简单的任务。为什么?主要答案是:为了使我们其余的代码更简单、更易于维护。事实上,我们可以使用本文前面部分描述的技术构建相同的布局。
.container {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
max-width: 960px;
}
.column {
--columns: 12; /* Number of columns in the grid system */
--width: 0; /* Default width of the element */
flex-basis: calc(var(--width) / var(--columns) * 100%);
}
.header {
--width: 12;
}
.content {
--width: 12;
}
.sidebar {
--width: 12;
}
@media (min-width: 576px) {
.content {
--width: 6;
}
.sidebar {
--width: 6;
}
}
@media (min-width: 768px) {
.content {
--width: 8;
}
.sidebar {
--width: 4;
}
}
在小型项目中,这种方法可以完美地工作。对于更复杂的解决方案,我建议考虑更具可扩展性的解决方案。
我为什么要费心呢?
如果提供的代码与我们使用 Sass 等预处理器可以实现的功能非常相似,我们为什么要费心呢?自定义属性是否更好?答案,通常是:视情况而定。使用 Sass 的优势是更好的浏览器支持。但是,使用自定义属性也有一些好处。
- 它是纯 CSS。换句话说,它是一个更标准、更可靠的解决方案,独立于任何第三方。无需编译,无需包版本,无需奇怪的问题。它可以正常工作(除了那些无法正常工作的浏览器)。
- 它更容易调试。这是一个有争议的问题,因为有人可能会争辩说 Sass 通过控制台消息提供反馈,而 CSS 则不提供。但是,您无法直接在浏览器中查看和调试预处理的代码,而在使用 CSS 变量时,所有代码都直接在 DevTools 中可用(且实时!)
- 它更易于维护。自定义属性使我们能够做一些预处理器无法做到的事情。它使我们能够使我们的变量更具上下文性,因此更易于维护。此外,它们可以通过 JavaScript 选择,而 Sass 变量则不能。
- 它更灵活。请注意,我们构建的网格系统非常灵活。您想在一页上使用 12 列网格,在另一页上使用 15 列网格吗?没问题——这只是一个变量的问题。相同的代码可以在两页上使用。预处理器需要为两个单独的网格系统生成代码。
- 它占用更少的空间。虽然 CSS 文件的权重通常不是页面加载性能的主要瓶颈,但我们仍然应该尽可能地优化 CSS 文件。为了更好地说明可以节省多少,我做了一个小实验。我从 Bootstrap 中获取了网格系统,并使用自定义属性从头构建了它。结果如下:Bootstrap 网格的基本配置生成超过 54KB 的 CSS,而使用自定义属性构建的类似网格仅为 3KB。这相差 94%!更重要的是,向 Bootstrap 网格添加更多列会使文件更大。使用 CSS 变量,我们可以根据需要使用任意数量的列,而不会影响文件大小。
可以压缩文件以稍微减少差异。压缩后的 Bootstrap 网格占用 6.4KB,而自定义属性网格占用 0.9KB。这仍然相差 86%!
CSS 变量的性能
总而言之,使用 CSS 自定义属性有很多优势。但是,如果我们让浏览器执行所有由预处理器完成的计算,是否会对我们网站的性能产生负面影响?确实,使用自定义属性和calc()
函数会消耗更多的计算能力。但是,在类似于本文中讨论的示例的情况下,差异通常是无法察觉的。如果您想了解更多关于此主题的信息,我建议阅读Lisi Linhart 的这篇优秀的文章。
不仅仅是网格系统
毕竟,理解自定义属性的来龙去脉可能并不像看起来那样容易。这肯定需要时间,但它是值得的。在处理可重用组件、设计系统、主题和可定制解决方案时,CSS 变量可以提供巨大帮助。了解如何处理回退值和未声明的变量可能会非常方便。
感谢您的阅读,并祝您在使用 CSS 自定义属性的旅程中好运!
谢谢,这太棒了!
如果您在提到以下内容时可以提供一些代码的之前/之后快照:
这篇文章会变得更好,如果可能的话!
这绝对是一个全新的文章主题 :) 但即使本文中的一些示例也与此相关。
例如,在 .column 类内设置 –columns 变量可以让你将其置于特定上下文中,并且不会污染全局命名空间。
为什么你总是用 * 100% 进行计算?我的意思是 *(100 / 100) 等于 * 1,它是乘法的单位元,这意味着它不会改变这个乘积中的任何东西。你不能只去掉它并节省另外 .x% 的文件大小吗?或者这是一种必要的浏览器 hack?我做过类似的事情,但考虑到 grid-gap 和可能在 border-boxed 父元素上的填充。
不确定您指的是哪个确切的示例,但如果我理解正确:我乘以 100% 以将值的单位设置为百分比。例如,”calc(var(–width) / var(–columns)”只会给你一个普通数字,比如 0.75。我将其乘以 100% 以使其变为 75%。