CSS指令

除了你已经熟悉的 @layer 和 @import,CSS 原生指令(专业术语叫 At-rules)在近几年简…

除了你已经熟悉的 @layer@import,CSS 原生指令(专业术语叫 At-rules)在近几年简直是“神仙打架”。它们让 CSS 从单纯的“样式表”变成了具有逻辑判断、作用域控制和复杂计算能力的语言。

以下是现代 Web 开发中最值得掌握的原生指令:

@import

@import 用于在一个 CSS 文件中引入另一个 CSS 文件。

1. 基本语法

重要规则: @import 必须位于 CSS 文件的最顶部(除了 @charset 之外),在任何其他 CSS 规则之前。

CSS

/* main.css */
@charset "UTF-8";

@import url("reset.css");
@import url("layout.css");
@import "typography.css"; /* url() 括号可以省略 */

/* 其他样式写在这里 */
body {
  background: #f0f0f0;
}
2. 现代高级用法 (条件引入)

你可以根据特定条件加载 CSS,这样可以节省带宽。

  • 媒体查询引入: 只有屏幕宽度小于 600px 时才下载/应用该 CSS。

    CSS

    @import url("mobile.css") screen and (max-width: 600px);
  • 级联层 (Cascade Layers) 引入: 配合现代 CSS 的 @layer 使用,更好管理优先级。

    CSS

    @import url("bootstrap.css") layer(framework);
    @import url("custom.css") layer(app);
3. ⚠️ 关键性能警告:慎用 @import

虽然 @import 写起来很方便,但在生产环境的高性能网站中,我们通常不推荐在 CSS 文件内部大量使用它。

为什么?(瀑布流加载问题)

  • 使用 <link> (推荐): 浏览器读到 HTML 时,会**并行(同时)**下载所有 CSS 文件。速度快。
  • 使用 @import (较慢):
    1. 浏览器下载 style.css
    2. 解析 style.css,发现第一行是 @import url("other.css")
    3. 暂停渲染,去下载 other.css
    4. 结果: 导致页面加载出现“串行”延迟,白屏时间变长。

@layer

这正是 CSS 在 2022 年左右推出的一个“核武级”特性:CSS 级联层(CSS Cascade Layers)

如果你曾经为了覆盖一个组件的样式,不得不写出类似 .header .nav .item .link.active { color: red !important; } 这种又臭又长的选择器,那么 @layer 就是你的救星。

1. 它解决什么痛点?

以前 CSS 判断谁生效,主要靠**“权重计算”**(选择器越长权重越高)。这导致了两个问题:

  1. 权重竞赛:大家为了覆盖样式,选择器越写越长,最后只能祭出 !important
  2. 加载顺序依赖:如果两个选择器权重一样,后加载的覆盖先加载的,这在打包工具(如 Webpack/Vite)合并代码时经常引发随机 Bug。

2. @layer 的逻辑:降维打击

@layer 允许你开启“上帝视角”,直接把 CSS 分成不同的优先级层不管层内的选择器权重多高,高优先级层里的样式永远覆盖低优先级层。

想象这是一个三明治:

你可以定义三个层:

  1. base (基础层:重置样式)
  2. library (框架层:比如 Bootstrap 或组件库)
  3. theme (自定义层:你的业务代码)
css
/* 声明层的顺序,后面写的层优先级更高 */
@layer base, library, theme;

@layer library {
  /* 即使这里权重很高,它也打不过 theme 层 */
  .button.primary.large { 
    background: blue; 
  }
}

@layer theme {
  /* 即使这里只是个简单的标签选择器,它也能赢! */
  button {
    background: red; 
  }
}

3. 它的惊人特性

A. 权重失效

在上面的例子中,即使 library 层的选择器非常精准(权重高),但因为它的层级排在 theme 之后,theme 里的 button 只要一行简单的代码就能轻松覆盖它。

B. 保护第三方库

你可以在引入外部 CSS 时直接把它扔进一个低优先级的层里:

css
@import url("bootstrap.css") layer(vendor);

这样你自己的代码只要不在 vendor 层,就永远不用担心被 Bootstrap 的复杂选择器给恶心到。

4. 浏览器的“级联”新排序

引入 @layer 后,浏览器判断样式的顺序变成了:

  1. !important(依然是老大,但现在建议少用)
  2. Cascade Layers(级联层) 👈 新加入的决策层
  3. Selector Specificity(选择器权重:ID > Class > Tag)
  4. Order of Appearance(出现顺序)

5. 什么时候用它?

  • 重构老项目:把老代码包在 @layer old_code 里,新代码写在 @layer new_code。你再也不用去删老代码,直接在新层里写简单的选择器就能覆盖。

  • 开发组件库:确保你的组件样式在底层,方便使用者在他们的代码层里随意定制。

使用教程

第一步:声明层的“优先级顺序”(核心步骤)

这是最重要的一步。你应该在 CSS 文件的最顶部,按优先级从低到高列出所有的层。

css
/* 越往后的层,优先级越高 */
@layer reset, base, components, theme, utilities;

为什么要这样做? 如果不预先声明,浏览器会按照它在代码中第一次出现的顺序来排优先级。为了防止混乱,顶层声明是最佳实践。

第二步:将样式分配到层中

你有三种方式可以把代码塞进这些“抽屉”里:

1. 使用块级语法(最常用)

就像写 @media 一样:

css
@layer base {
  h1 { font-size: 2rem; color: black; }
  p { line-height: 1.6; }
}
2. 引入外部文件时直接分层

这是“降维打击”第三方库的神技:

css
/* 把整个 Bootstrap 塞进底层的 reset 层,防止它干扰你的业务代码 */
@import url("https://cdn.bootstrap.com/...) layer(reset);
3. 命名空间形式(嵌套层)

如果你想更细致地管理,还可以写嵌套层:

css
@layer components {
  @layer buttons {
    .btn { padding: 10px; }
  }
}
/* 也可以简写为 */
@layer components.buttons {
  .btn { border: none; }
}
/*提前声明优先级*/
@layer components.buttons, components.cards;
/*按照默认出现顺序*/
@layer components {
  /* 1. 先出现的子层,优先级低 */
  @layer buttons {
    .btn { color: red; }
  }
  /* 2. 后出现的子层,优先级高 */
  @layer alerts {
    .btn { color: blue; }
  }
}
/* 最终结果:.btn 是蓝色的 */

嵌套层最厉害的地方在于,你可以像访问对象属性一样,用 “点” (.) 操作符在任何地方补充样式,而不需要写那层厚厚的“外壳”

css
/* 直接给 components 里的子层 buttons 补样式 */
@layer components.buttons {
  .btn {
    border-radius: 5px;
  }
}

第三步:理解层的“优先级”逻辑(重点)

这里有一个反直觉但极其强大的规则:层级胜过权重。

css
@layer base, theme;

@layer base {
  /* 这个选择器权重很高 (ID + Class),但在 base 层 */
  #main-title.important {
    color: blue;
  }
}

@layer theme {
  /* 这个选择器权重极低 (标签选择器),但在更高的 theme 层 */
  h1 {
    color: red; /* 最终结果:红色! */
  }
}

浏览器一旦发现 theme 层的优先级高于 base 层,它就会直接忽略 base 层里所有的权重计算,直接采用 theme 层里的定义。

第四步:检查层级(开发者工具)

现在打开 Chrome 或 Edge 的控制台(F12),切换到 Elements -> Styles 面板。你会发现样式旁边多了一个类似 (layer: theme) 的标记。如果某个样式被层级压制了,它会被划掉,并明确告诉你它输给了哪个层。

避坑指南:

  1. 非图层样式(Unlayered Styles)的统治地位: 如果你写了一段不在任何 @layer 里的代码,浏览器认为这是**“最高优先级”**。它会覆盖所有 @layer 里的样式。

逻辑顺序: !important > 非图层样式 > 自定义层(高) > 自定义层(低) > 浏览器默认样式

  1. !important 的奇葩反转: 这是一个非常烧脑的设定:如果在 @layer 里用了 !important低优先级层!important 居然会赢过 高优先级层!important。这是为了让底层样式(如 Reset)在极端情况下能“翻身”。(建议:既然用了 @layer,就彻底告别 !important 吧!)

总结一个“万金油”层级模板:

你可以把这段代码放在你项目的全局 CSS 头部:

css
/* 定义顺序 */
@layer reset, vendor, base, components, utilities;

@layer reset { /* 放置 CSS Reset 或 Normalize.css */ }
@layer vendor { /* 放置第三方插件的 CSS */ }
@layer base   { /* 放置全局标签样式,如 body, h1, a */ }
@layer components { /* 放置你写的业务组件 */ }
@layer utilities  { /* 放置类似 .mt-2, .hidden 这种原子类 */ }

Tailwind关系

事实上,这是一个非常经典的**“技术反哺标准”**的过程:

谁先谁后?

  • Tailwind CSS 的 @layer:它很早就存在了(早在 2019-2020 年左右)。但那时候它是一个伪指令,浏览器根本不认识。它是通过 PostCSS 工具在编译阶段,把你写的代码手动“搬运”到生成的 CSS 文件的不同位置。
  • 原生 CSS 的 @layer:它是 2022 年才正式成为浏览器标准的(CSS Cascading and Inheritance Level 5)。

真相是: Tailwind 设计了这个好用的概念,大家发现这种“分层管理样式”的思路太牛了,于是 W3C 就把它正式收编进了 CSS 官方全家桶,变成了浏览器原生支持的功能。

虽然名字一样,但底层逻辑完全不同:

特性Tailwind 的 @layer (编译时)原生 CSS 的 @layer (运行时)
生效原理物理排序。它只是把你的代码搬到 basecomponentsutilities 的对应位置。逻辑排序。它告诉浏览器:这一块代码即使在后面,如果层级低,它也得让路。
选择器权重依然重要。在同一个层内,你还是得靠权重竞争。降维打击。高优先级的层可以直接无视低优先级层里的复杂权重。
浏览器可见不可见(编译后就消失了,变成了普通的 CSS)。可见。你在 Chrome 开发者工具里能直接看到样式属于哪个 layer

原生 @layer 解决了一个 Tailwind 解决不了的痛点

在 Tailwind 里,如果你引入了一个第三方的 CSS 插件(比如一个复杂的日历组件),你很难通过 Tailwind 的指令去精准控制那个插件的优先级。

但在原生 CSS 中,你可以直接给外来的文件“降级”:

css
/* 管你 Bootstrap 里面写了多少 !important (其实还是怕 !important,但普通权重能压死) */
/* 只要我把你定义在 lowest 层,你就得听我的 */
@import url("bootstrap.css") layer(lowest);

@container:容器查询(响应式的终结者)

它解决了前端开发中一个存在了十几年的痛点:同一个组件,放在窄的地方(如侧边栏)和宽的地方(如主体区)应该有不同的长相,但以前只能靠写死不同的 Class 来实现。

这才是真正的“组件化”:一个卡片组件,放在窄的侧边栏里自动变成垂直布局,放在宽的中间内容区自动变成横向布局,而它根本不需要知道屏幕有多大

第一步:定义“容器”(给父元素盖章)

要让子元素能够感知宽度,必须先告诉浏览器哪个元素是“参考坐标系”。你需要在父元素上设置 container-type

css
.parent {
  /* inline-size 表示只观察容器的宽度(最常用) 另外一个应该是 block-size */
  /* size 表示同时观察容器的宽度和高度 */
  container-type: inline-size;
  
  /* (可选)给容器起个名字,防止嵌套干扰 */
  container-name: sidebar-card;
}

第二步:编写“容器查询”(子元素变身)

子元素不再去问屏幕有多宽,而是去问父元素。语法和 @media 极其相似:

css
/* 当父容器宽度大于 400px 时 */
@container (min-width: 400px) {
  .child {
    display: flex;
    gap: 20px;
    background: #f5f5f5;
  }
}

第三步:为什么它比 @media 强?(实战场景)

想象你在做一个“用户信息卡片”:

  1. 场景 A:你把它放在页面的侧边栏(宽度 300px)。
  2. 场景 B:你把它放在页面的顶部导航(宽度 1200px)。
  • @media:你发现屏幕都是 1920px 宽,所以 @media 没法区分这两个卡片。你不得不手动写 .card-sidebar.card-header 两个类。
  • @container:你只需要写一套逻辑。卡片会自动发现:
    • “哦,我在侧边栏,父级只有 300px,我应该垂直堆叠。”
    • “哇,我在顶部,父级有 1200px,我应该横向排开。”

第四步:容器查询单位 (Container Query Units)

为了配合容器查询,CSS 还引入了一套全新的单位。它们不再相对于视口(vw/vh),而是相对于容器

单位含义对应视口单位
cqw容器宽度的 1%vw
cqh容器高度的 1%vh
cqi容器内联方向尺寸的 1% (通常是宽)vi
cqb容器块级方向尺寸的 1% (通常是高)vb
cqmin / cqmax取 cqw 和 cqh 中较小/较大的值vmin / vmax

font-size: 5cqw; —— 字号会随着父盒子的变大而变大,这才是真正的响应式组件!

第五步:进阶技巧——具名容器 (Named Containers)

如果你的页面很复杂,存在多层嵌套容器,你可以指定子元素去找哪个容器:

css
.sidebar { container: sidebar-area / inline-size; }
.main { container: main-area / inline-size; }

/* 明确指定:我要根据 sidebar-area 的宽度来变 */
@container sidebar-area (min-width: 200px) {
  .user-avatar { width: 50px; }
}

@scope:样式隔离(原生“局部样式”)

痛点:CSS 样式是全局的,容易互相污染。以前我们靠 Vue 的 scoped 或 CSS Modules 解决。

神技:它能把样式限制在特定的 DOM 树范围内,而不需要复杂的类名。

1. 核心语法:定义“边界”

@scope 的基本语法包含两个部分:起始点(Root)结束点(Limit)

A. 基础用法(只有起始点)
css
/* 只有在 .card 元素内部的 p 标签才会变成蓝色 */
@scope (.card) {
  p {
    color: blue;
  }
}

这看起来很像普通的嵌套,但它比 .card p 更高效,因为它建立了一个明确的作用域上下文

B. 高级用法:中空作用域(起始点 + 结束点)

这是 @scope 最神奇的地方。你可以指定样式在碰到某个子元素时停止生效。这被称为“多孔(Donut)作用域”。

css
/* 样式从 .card 开始,但在碰到 .content-slot 时停止 */
@scope (.card) to (.content-slot) {
  p {
    color: red;
  }
}

实际效果.card 里的 p 是红色的,但如果 .card 里面嵌套了一个 .content-slot,那么这个插槽内部的 p 不会受到影响。

2. 特殊符号::scope

@scope 块内部,你可以使用 :scope 伪类来指代当前的作用域根元素(即括号里的那个元素)。

css
@scope (.card) {
  :scope { 
    border: 1px solid black; /* 指代 .card 自身 */
    background: white;
  }
  img { border-radius: 50%; }
}

3. @scope 的三大核心优势

① 解决“权重竞赛”(Proximity vs Specificity)

这是 @scope 最硬核的逻辑:就近原则胜过权重。 如果两个作用域都定义了同一个元素的样式,浏览器会优先选择“离目标元素更近”的那个作用域,而不是计算复杂的 ID/Class 权重。

② 真正的局部样式(不用改类名)

你可以在不同的 @scope 里写相同的标签选择器,而不用担心它们打架。

css
@scope (.dark-theme) {
  button { background: black; color: white; }
}

@scope (.light-theme) {
  button { background: white; color: black; }
}
③ 解决“过度渗透”问题

传统的嵌套选择器 .card p 会选中 .card 内部所有层级p。而 @scope 配合 to 语法,可以让你精准控制样式只影响“这一层”,而不波及“嵌套的下一层”。

4. 结合 HTML 直接使用(内联 Scope)

虽然不常用,但你甚至可以在 HTML 中直接开启作用域(类似早期被废弃的 scoped 属性,但现在更强大):

html
<div class="user-profile">
  <style>
    @scope {
      img { width: 50px; } /* 这里的 img 只影响 user-profile 内部 */
    }
  </style>
  <img src="avatar.jpg">
</div>

5. 与 @layer 的联动

你可以把 @scope 嵌套在 @layer 内部,实现完美的样式管理:

  1. @layer:管理不同来源的优先级(如框架 vs 业务)。
  2. @scope:管理不同组件的势力范围(如导航栏 vs 侧边栏)。
css
@layer components {
  @scope (.tabs) {
    .item { padding: 10px; }
  }
}

⚠️ 兼容性提醒

@scope 是非常新的特性(Chrome 118+ 已支持)。在生产环境使用前,建议结合 @supports 检测:

css
@supports selector(:scope) {
  /* 现代浏览器代码 */
}

@property:赋予 CSS 变量“生命”

痛点:原生的 CSS 变量(--color)只是字符串,没法做动画(比如你没法让背景渐变色丝滑过渡)。

神技:它允许你定义变量的类型(数字、颜色、百分比)。

css
@property --angle {
  syntax: '<angle>'; /* 定义它是角度类型 */
  initial-value: 0deg;
  inherits: false;
}

.box {
  background: conic-gradient(from var(--angle), red, blue);
  transition: --angle 2s; /* 现在变量可以跑动画了! */
}
.box:hover { --angle: 360deg; }

1. 历史背景:从 var()@property

第一阶段:远古时代的 Hardcode

以前,我们要改主题色,得在 CSS 里全局搜索替换 #ff5722。那是黑暗时代。

第二阶段:CSS 变量 var() 的诞生(约 2014 年)

CSS 变量的出现彻底改变了游戏规则。你可以定义一个变量:

css
:root {
  --main-color: #ff5722;
}
.btn {
  background-color: var(--main-color);
}

痛点出现: 虽然 var() 很好用,但它在浏览器眼里只是个**“无情的字符串”**。

不能做动画:如果你想让背景渐变色从红色平滑过渡到蓝色,浏览器会直接“罢工”。因为它不知道 --color 是一个颜色,它只觉得那是串字符。

没有默认值/类型检查:如果你不小心把 --width: red; 给了一个需要像素值的属性,代码直接静默失效,排查起来极其痛苦。

第三阶段:@property 的降临(Houdini 计划的一部分)

为了解决上述问题,CSS 推出了 @property。它不仅是定义变量,而是**“注册”变量——告诉浏览器这个变量的数据类型**。

2. @property 实战教程

它的语法非常严谨,看起来像是在写某种强类型语言(如 TypeScript):

css
@property --my-score {
  syntax: '<number>';      /* 1. 定义数据类型(数字、颜色、百分比等) */
  inherits: false;         /* 2. 是否允许子元素继承 */
  initial-value: 0;        /* 3. 初始值(必填,除非语法允许为空) */
}
核心配置项说明:

syntax: 关键点!常见值有 <color>, <length>, <percentage>, <number>, <angle> 等。

inherits: 如果设为 true,父元素改了值,子元素也会跟着变。

3. @property 的超能力:动画

这是它最惊艳的用途。我们来做一个**“平滑渐变动画”**,这在以前不用 JS 是绝对做不到的。

css
/* 注册变量 */
@property --bg-color {
  syntax: '<color>';
  inherits: false;
  initial-value: red;
}

.box {
  width: 200px;
  height: 200px;
  background-color: var(--bg-color);
  /* 关键:现在浏览器知道它是颜色,可以进行差值计算了!关键就是可以对变量设置过度动画了! */
  transition: --bg-color 1s ease;
}

.box:hover {
  --bg-color: blue;
}

4. 进阶神技:数字跑表

因为浏览器现在认识了 <integer>(整数)类型,我们可以直接用 CSS 做一个数字跳动动画:

css
@property --num {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.counter::after {
  counter-reset: my-count var(--num);
  content: counter(my-count); /* 把变量转为内容显示 */
  animation: count-up 5s forwards;
}

@keyframes count-up {
  from { --num: 0; }
  to { --num: 100; }
}

5. 总结:var()@property 的区别

特性var() (Custom Properties)@property (Registered Properties)
定义位置选择器内部(如 :rootCSS 顶层(全局)
类型感知无(仅字符串)有(强类型)
动画支持极差(无法补间动画)完美支持
安全性容易被错误的值覆盖有初始值保护,容错率高
JS 交互getPropertyValue可以通过 CSS.registerProperty() 注册

@supports:特性检测(优雅降级)

痛点:有些新属性(如 display: grid)在旧浏览器会崩。

神技:在 CSS 里写逻辑判断:“如果浏览器支持这个功能,就执行这段代码”。

1. 基础语法:三大逻辑运算符

你可以在 @supports 中使用 and(且)、or(或)和 not(非)。

A. 检查多个属性 (and)
css
/* 只有同时支持 grid 布局和容器查询时,才应用此样式 */
@supports (display: grid) and (container-type: inline-size) {
  .layout {
    display: grid;
    container-type: inline-size;
  }
}
B. 兼容性备选 (or)
css
/* 只要支持其中一种现代定位方式即可 */
@supports (position: sticky) or (position: -webkit-sticky) {
  .header {
    position: sticky;
    top: 0;
  }
}
C. 否定逻辑 (not)
css
/* 如果浏览器不支持新的 aspect-ratio,就用旧的 padding 方案补丁 */
@supports not (aspect-ratio: 1/1) {
  .box {
    width: 100%;
    padding-top: 100%; /* 模拟 1:1 */
    position: relative;
  }
}

2. 高级用法:检测“选择器”支持

这是很多开发者容易忽略的功能。有时候我们不是要检测一个属性(如 color),而是要检测浏览器认不认识某个新的选择器(如 :has():is())。

语法:selector(...)

css
/* 检测浏览器是否支持“父级选择器” :has() */
@supports selector(:has(p)) {
  .card:has(img) {
    background: #f0f0f0;
  }
}

3. 进阶实战:结合“CSS 变量”

你可以用 @supports 来定义一套“现代版”的变量系统:

css
:root {
  --main-bg: #fff;
}

/* 如果支持高级的颜色函数(比如 OKLCH),就启用更鲜艳的色彩系统 */
@supports (color: oklch(0% 0 0)) {
  :root {
    --main-bg: oklch(60% 0.15 250);
  }
}

@keyframes:动画灵魂

这个你可能见过,它是定义 CSS 动画序列的核心:

css
@keyframes slideIn {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

@media

@font-face

@media (scripting)

这是 渐进增强 (Progressive Enhancement) 架构师的必备工具。

  • 场景:
    • 你做了一个复杂的图片轮播组件,完全依赖 JavaScript 来运行。
    • 痛点: 如果用户的浏览器禁用了 JS,或者 JS 文件加载失败(网络波动),页面上就会留下一堆动不了的、布局错乱的 HTML 骨架。
    • 以前的做法: <noscript> 标签(只能显示文本,很难控制样式)。
  • 现代解法: 直接在 CSS 里检测“JS 是否可用”。

CSS

/* 默认情况(假设 JS 挂了):显示一个简单的、可滚动的列表 */
.gallery {
  overflow-x: auto;
  display: flex;
}

/* 核心:只有当脚本(JS)可用时,才应用这些样式 */
@media (scripting: enabled) {
  .gallery {
    /* 把列表隐藏,初始化为复杂的 3D 轮播图样式 */
    overflow: hidden;
    perspective: 1000px;
    /* ...其他依赖 JS 的样式... */
  }
}

底蕴价值: 这体现了你对 系统健壮性 (Robustness) 的考量。无论环境多么恶劣(企业内网禁用 JS、爬虫访问、老旧设备),你的页面都能“优雅降级”,而不是直接崩坏。

6. 还有一些你可能见过但没细想的:

  • @font-face:引入自定义字体。
  • @media:除了看宽度,现在还能看:
    • @media (prefers-color-scheme: dark):检测用户是否开启了深色模式
    • @media (prefers-reduced-motion: reduce):检测用户是否关闭了系统动画(为了防眩晕)。
  • @page:控制打印页面时的边距、分页符。

总结你的“指令工具箱”

指令核心用途推荐指数
@layer解决优先级冲突(权重地狱)⭐⭐⭐⭐⭐
@container组件级响应式(不再管屏幕宽度)⭐⭐⭐⭐⭐
@scope样式隔离,防止污染⭐⭐⭐⭐
@property让 CSS 变量支持动画过渡⭐⭐⭐
@supports确保新特性在旧浏览器的兼容性⭐⭐⭐⭐

评论

评论加载中……