CSS 教程 / 用共享样式、嵌套与层叠层组织 CSS

可维护的 CSS 会根据其角色分离共享值、基础样式、组件和条件性调整。原生嵌套把相关选择器集中书写,而层叠层则显式地排列规则组并引入它们自己的优先规则。

减少重复并分配文件职责

首先,查找相同颜色和间距的重复项、已经不再使用的选择器以及几乎相同的按钮规则。具有相同作用的值会被分组到变量中,不同的按钮会划分公共规则和必要的差异。可能更好的是保留两个独立的组件,而不是创建复杂的条件来消除所有重复项。在分组公共值时,检查 CSS 变量中的变量作用域和继承条件:var()、作用域、继承和回退 。

/* Before cleanup: Same value repeated in multiple places */
.lesson-button { padding: .65rem 1rem; color: #166b92; }
.notice-button { padding: .65rem 1rem; color: #166b92; }

/* After organizing: sharing common shapes and roles */
.button { padding: .65rem 1rem; color: var(--accent); }
.button.text-alert { color: #9d2632; }

上述比较是解释清理方向的部分代码。一个通用按钮类也必须附加到实际的 HTML,如果你只是重命名 CSS 而保持 HTML 不变,外观可能会消失。下面的整个练习通过文件和层来运行该配置。

文件 责任 修改时的判断
tokens.css 常用颜色和间距 这是多个组件共享的值吗?
base.css 字体、默认边距、焦点显示 这是整个文档的默认行为吗?
layout.css 内容区域和布局 这是组件之间的位置关系吗?
components.css 卡片和按钮的形状和状态 它绑定到某个组件吗?
utilities.css 小而明确的辅助类 这是只在少数地方应用的例外吗?

小型文档可以先在同一个文件内用注释划分区段。当多个人修改不同组件或重用范围增加时,考虑分离文件。文件名是导航的分隔符,仅仅分割文件并不能消除选择器冲突。HTML 连接路径和层叠关系也必须进行管理。

区分嵌套中的当前元素与后代

原生 CSS 嵌套由浏览器直接解释。这与 Sass 等预处理器在构建阶段生成不同 CSS 的方式不同。下面的代码将 .card 的标题与卡片本身的状态放得很近。

.card {
  & h2 { color: var(--accent); }
  &:hover { border-color: var(--accent); }
}

& h2 代表卡片内的 h2,&:hover 代表卡片本身的悬停状态。如果插入一个空格,例如 & :hover,结果会不同,因为它选择的是被悬停的后代。同时要区分 &.warning,它选择同一元素的辅助类,以及 & .warning,它选择后代元素。

/* The simple overlap above is expanded */
.card h2 { color: var(--accent); }
.card:hover { border-color: var(--accent); }

使用这个单父选择器,可以将嵌套与展开后的规则直接进行比较。对于逗号分隔的父选择器列表,嵌套规则中的父选择器部分按该列表的最高优先级计算;即使其中的 ID 分支没有匹配当前元素,也会影响这个优先级。因此,复杂的嵌套并不是简单的字符串拼接。避免不必要的深度,这会将 CSS 绑定到僵硬的 HTML 层次结构。更多细节,请参阅 MDN 嵌套使用指南、 嵌套和优先级解释。

检查你需要支持的浏览器的嵌套支持情况,如果你需要一个不支持嵌套的环境,请按上文选择展开的 CSS 或使用适当的构建工具。仅仅因为你使用了新的语法,并不意味着它适用于你所有的访问者。

在优先级比较之前比较级联层

对于同一作者来源的样式表中的普通声明,层外声明优先于层内声明。在层内,按预定顺序,后面的层优先于前面的层。因此,后层中的类规则可以胜过前层中的ID规则。如果其他比较条件如相同层和重要性相同,则随后判断CSS继承性、特异性和层叠、作用范围的邻近性以及声明顺序等条件。

@layer tokens, base, layout, components, utilities;
@layer base { #layer-target { color: #183044; } }
@layer utilities { .text-alert { color: #9d2632; } }

示例按钮匹配两个选择器。本例比较普通声明,utilities 层在后,因此红色生效。仅仅因为按钮具有高 ID 优先级就预测结果可能是错误的。图层的顺序由它们名称首次注册的顺序决定,即使你稍后再次打开同一图层,该图层也不会被移到后面。

!important 声明在本例同一作者来源的样式表中优先于普通声明,并且重要声明的层顺序是反向的。先注册层的 !important 声明优先于后注册层的 !important 声明;已分层的重要声明优先于本例未分层的重要声明。本示例仅限于比较同一作者 CSS 的层及其属性。不能据此忽略用户/浏览器样式、内联声明和正在运行的过渡等其他层叠条件。您可以在 MDN:@layer 中查看完整条件。

创建 HTML 和五个 CSS 文件

在一个空文件夹中创建以下结构。所有文件均以 UTF-8 格式保存。由于 index.html 使用相对路径 styles/…,CSS 文件必须位于 styles 子文件夹中。你可以在没有单独服务器或外部库的情况下使用浏览器打开 index.html。如果无法读取文件,请将文件位置和连接地址与 通过行内、内部和外部样式应用 CSS 对比。

stylesheet-project/
  index.html
  styles/
    tokens.css
    base.css
    layout.css
    components.css
    utilities.css

index.html 包含实际的链接标签和类。比较按钮并不是执行操作的按钮,而是为了练习检查已应用的文本颜色和焦点。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CSS files and layers by responsibility</title>
  <link rel="stylesheet" href="styles/tokens.css">
  <link rel="stylesheet" href="styles/base.css">
  <link rel="stylesheet" href="styles/layout.css">
  <link rel="stylesheet" href="styles/components.css">
  <link rel="stylesheet" href="styles/utilities.css">
</head>
<body>
  <main class="demo">
    <h1>Organizing a small stylesheet</h1>
    <article class="card">
      <h2>Scope nested rules to the card</h2>
      <p>The card heading uses the accent color. Compare it with the heading outside the card.</p>
      <button class="button" type="button">Regular button</button>
    </article>
    <section><h2>Heading outside the card</h2><p>Use the default font color.</p></section>
    <button id="layer-target" class="button text-alert" type="button">Layer comparison button</button>
  </main>
</body>
</html>

styles/tokens.css 首先注册层的顺序并声明常用值。另一个文件使用相同名称打开层以向其添加规则。

@layer tokens, base, layout, components, utilities;
@layer tokens {
  :root { --ink: #183044; --accent: #166b92; --space-card: 1.25rem; }
}

styles/base.css 设定了默认的文档外观和焦点指示器。ID 规则是为比较层顺序对优先级的影响而特意加入的对照规则。

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; padding: 1rem; color: var(--ink); font-family: sans-serif; line-height: 1.6; }
  h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
  button { font: inherit; cursor: pointer; }
  :focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
  #layer-target { color: #183044; }
}

styles/layout.css 管理内容区域和章节之间的间距。

@layer layout {
  .demo { display: grid; gap: 1rem; max-width: 44rem; margin-inline: auto; }
  .demo > h1 { margin-block: 0 .5rem; }
  .demo > button { justify-self: start; }
}

styles/components.css 管理卡片和按钮。标题颜色只应用于卡片内部,鼠标悬停在卡片上或聚焦卡片内部的按钮时,边框会发生变化。

@layer components {
  .card {
    padding: var(--space-card); border: 2px solid #90a4b2; border-radius: .5rem;
    & h2 { margin-top: 0; color: var(--accent); }
    &:hover, &:focus-within { border-color: var(--accent); }
  }
  .button {
    padding: .65rem 1rem; color: var(--accent); background: #eaf4fa;
    border: 1px solid #166b92; border-radius: .3rem;
  }
}

styles/utilities.css 管理用于比较按钮的辅助类。此文件中的一般声明优先于前几层中的 ID/按钮规则。

@layer utilities {
  .text-alert { color: #9d2632; }
}

独立测试文件、嵌套和层

  1. 在主屏幕上,确保卡片标题和常规按钮为蓝色,卡片外的标题为默认颜色,比对按钮为红色。按 Tab 键移动焦点,检查按钮的焦点指示。
  2. 只注释掉 components.css 中卡片内的 & h2 规则。卡片标题也会恢复到默认颜色。确认后,再取消注释。
  3. 给 HTML 中卡片内部的按钮加上 text-alert,使两个按钮都具有这个类。内部按钮也会变红。比较后,仅从内部按钮删除新增的 text-alert,保留原有 button 类。
  4. 在 components.css 的末尾,仅在 @layer 块之外添加 .button { color: #28633b; }。两个按钮都会变绿。这是未分层的普通声明优先于本例各层内普通声明的结果。确认后,删除新的规则。
  5. 仅在 base.css 中的 ID color 声明上添加 !important。比较按钮会变为默认颜色。在该状态下,如果在 utilities.css 的 color 上添加 !important,则会保留上一层的基础颜色。比较后,删除两处 !important。
  6. 只需暂时从 HTML 中移除 components.css 链接。卡片和按钮的组件形状会消失。将路径恢复到原始状态并刷新即可恢复。

诊断缺失的样式

分别检查文件分离问题和选择器冲突。首先在浏览器开发者工具中查看网络(Network)或资源(Sources),以确认文件是否实际加载,然后选择元素,查看样式(Styles)中的规则是否匹配,以及是否有任何声明被覆盖。如果看不到文件,检查相对路径、文件名和扩展名;如果看不到规则,检查类名和语法支持。

即使存在匹配规则,如果出现删除线,也会比较层、重要性、选择器、作用域和顺序。如果公共变量不符合预期,还需查看变量声明的作用域。分离后,重新检查主屏幕和焦点/悬停是有助于维护的,同时修复导致冲突的责任划分和选择器,而不是继续添加不必要的 !important 声明。