CSS现代技巧

:is() 接受一个选择器列表,匹配其中任意一个选择器的元素。

:is&:where&has

is&where

css
:is(selector1, selector2, selector3)

:is() 接受一个选择器列表,匹配其中任意一个选择器的元素。

让我们以 summary:is(details:hover > summary, details[open] > summary) 为例,拆解它的全路径解析过程:

在括号内部,浏览器为了性能,采取的是从右往左的匹配策略。它不是先找 details,而是先在页面上抓取所有的 summary

当浏览器遇到 summary:is(...) 时,它的心理活动是这样的:

  1. 第一步(过滤主体): “先在 DOM 树里找出所有的 <summary> 标签。剩下的元素(比如 div, p)我都不看了。”
  2. 第二步(进入 :is() 内部验证): “现在,我要看看这些 summary 有没有资格穿上这件名为 :is() 的衣服。我要拿括号里的两个‘模板’去套。”
    • 模板 A (details:hover > summary): “我看看这个 summary亲爹是不是一个正在被悬停的 details?”
    • 模板 B (details[open] > summary): “如果模板 A 不行,那我再看看它的亲爹是不是一个处于打开状态的 details?”
  3. 第三步(逻辑合并): “只要上面两个模板有一个套中了,那这个 summary 就正式被我‘锁定’了。”

接下来如果是:summary:is(details:hover > div, details[open] > span)

外面(主体):你告诉浏览器,我们要找的是一个 <summary>

里面(过滤器):你告诉浏览器,这个东西必须匹配 details:hover > div

解析结果:浏览器会发现,一个元素不可能既是 <summary> 又是 <div>

:is() 括号里的内容,必须能和括号外的元素“合体”成功,这个选择器才会有意义。

summary:is(.container) $\approx$ summary:is(&.container)

在 CSS 中,单独写 :is(.container) 实际上等同于 *:is(.container),这里的 * 是被隐式补全的。

:is() 的优先级不是由它自己决定的,而是由它括号里最重的那个选择器决定的。

:where() 是为了解决“样式覆盖战”而生的。无论你往括号里塞什么(哪怕是 ID),它的权重永远是 0

希望用户能轻松覆盖你的默认样式,你应该用 :where()

使用 :is() 简化逻辑并保持强度

has

:has() 选择器被誉为 CSS 的**“救世主”**,因为它解决了 CSS 诞生 30 多年来最大的遗憾:没有“父级选择器”

:has() 出现之前,CSS 的选择器是“单向”的(只能从父到子)。而 :has() 让 CSS 具备了**“向后看”“向上看”**的能力。

也就是父亲的状态取决于儿子的状态

css
/* 只有当 details 内部包含一个类名为 .content 的元素时,才给 details 加背景 */
details:has(.content) {
  background: #fff;
}

解析步骤:

  1. 定位主体:浏览器首先扫描所有的 details 元素。
  2. 触发探测:对于每一个 details,浏览器会进入其内部(子树)寻找是否存在 .content
  3. 匹配结果
    • 如果找到了:给这个特定的 details 应用样式。
    • 如果没找到:跳过,继续看下一个。

如果 .card 后面紧跟着一个 .banner,就给 .card 增加底边距:

css
.card:has(+ .banner) {
  margin-bottom: 50px;
}

简单来说::has() 括号里的内容,默认就带了一个“后代选择器”(即那个看不见的空格)。

在 CSS 规范中,如果你在 :has() 的括号内不写任何连接符,它会默认认为你在寻找后代(Descendants)

为了让你彻底理解,我们对比一下这几种写法:

写法逻辑含义对应非 :has() 路径
form:has(input)后代:表单里有 input(不论多深)。form input
form:has(> input)子代:表单的第一层级有 input。form > input
form:has(~ div)通用兄弟:表单后面跟着个 div。form ~ div
form:has(+ div)相邻兄弟:表单后面紧挨着个 div。form + div

那可不可以有“去除”那种空格的方式?就比如.container.wrap

可以这么写:.container:has(&.wrap)

css
/* 只有当 .container 自身匹配 .wrap 时(这通常在 JS 动态添加类名时很有用) */
.container:has(&.wrap) {
  /* 这里的逻辑比较绕,现代浏览器通常支持嵌套引用来检测自身 */
}

&CSS 嵌套模块(CSS Nesting) 的专属符号,它必须写在一个大括号内部才有意义。

.container:has(&.wrap)。此时 & 处于顶层,它没有“父亲”可以引用,浏览器解析到这里会认为这是一个非法选择器,直接丢弃整条规则。

在原生 CSS 中,& 必须作为嵌套选择器的“开头”或者“一部分”来引用父级,且目前它无法在 :has() 的括号内实现你想象中的“自我检测”功能。

:not()

现在的 :not() 已经支持多个参数了,不再需要套娃写好几个 :not()

css
/* 选中不是 active 也不是 disabled 的所有按钮 */
button:not(.active, .disabled) {
  cursor: pointer;
}

其它超好用的现代选择器

:focus-within:只要子元素里有任何一个获得了焦点,父元素就变样(常用于表单高亮)。

:empty 的进化版 :blank:不仅检测有没有子元素,还检测里面是不是只有空格。

:nth-child(An+B of S):这是超强的组合技!在特定类名里筛选第几个。

你肯定用过 :nth-child(2)(选第 2 个子元素)。但如果 HTML 结构很乱,你想选“第 2 个加了 .active 类的子元素”,以前是做不到的。

html
<div>
  <p>干扰项</p>
  <p class="item">目标 1</p>
  <p>干扰项</p>
  <p class="item">目标 2</p> 
</div>

如果你写 .item:nth-child(2),它选中的是第 2 个子元素(也就是第一个 .item),因为浏览器是先数位置,再看类名。现代解法: 使用 of 关键字。

css
/* 翻译:在所有的 .item 里面数数,选第 2 个 */
/* 完美选中 "目标 2",直接忽略干扰项 */
:nth-child(2 of .item) {
  color: red;
}

底蕴价值: 这是逻辑过滤。它解决了 CSS 选择器长期以来“数数不准”的顽疾,让你可以精准打击 DOM 树深处的特定元素。

CSS嵌套取代Sass&Less

原生 CSS 嵌套(CSS Nesting)已经在 2023 年全面进入了所有主流浏览器(Chrome 112+, Firefox 117+, Safari 16.5+)。

这意味着你以前为了使用 & 嵌套而不得不配置 Sass/Less 编译环境的日子,正式成为历史了。

现在的原生语法怎么写?

现在的原生 CSS 几乎完美复刻了 Sass 的嵌套逻辑,甚至更宽容:

css
/* 纯 CSS 文件,无需编译 */
.card {
  background: white;
  padding: 20px;
  /* 1. 嵌套子元素 */
  .title {
    font-size: 1.5rem;
  }
  /* 2. 使用 & 引用父级(状态切换) */
  &:hover {
    border-color: blue;
  }
  /* 3. BEM 风格连接(注意:这是原生与 Sass 的唯一大区别) */
  /* 原生 CSS 不支持像 Sass 那样直接拼接字符串:&__title 是无效的 */
}

原生嵌套 vs Sass 的关键区别

虽然看起来一模一样,但有几个细节你一定要知道:

关于“字符串拼接” (BEM)

  • Sass: 支持 &__header,编译后变成 .card__header
  • 原生 CSS: 不支持& 在原生 CSS 中代表的是一个完整的选择器(或选择器列表),它不能被当作字符串前缀。
  • 建议:在原生时代,大家更倾向于直接嵌套 .title 或者使用 :has() 这种更高级的选择器。

动态视口单位

动态视口单位 (dvh, svh, lvh)

在移动端(尤其是 iOS Safari),浏览器的地址栏会伸缩,导致 100vh 经常出问题(内容被底部栏遮挡)。

  • svh (Small Viewport Height): 地址栏展开时的可见高度(最短)。
  • lvh (Large Viewport Height): 地址栏收起时的可见高度(最长)。
  • dvh (Dynamic Viewport Height): 推荐使用。它会自动随地址栏伸缩而变化,完美解决移动端全屏问题。

滚动驱动动画:animation-timeline

这是目前的最前沿黑科技(Scroll-driven Animations)。

痛点: 以前要做“页面顶部阅读进度条”或者“随着滚动,图片慢慢变大”的效果,必须写 JS 监听 scroll 事件,计算 scrollTop,性能很差。

现代解法: CSS 直接绑定滚动条作为“时间轴”。

css
/* 定义一个进度条动画 */
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 5px;
  background: red;
  transform-origin: 0 50%;
  
  /* 核心:将动画进度与页面滚动进度绑定 */
  animation: grow-progress linear;
  animation-timeline: scroll(); 
}

第一阶段:石器时代 (jQuery & window.onscroll)

大约 2010 - 2015 年

这个时候,如果你想做一个“随着滚动,导航栏变透明”的效果,你必须写 JavaScript。

做法: 监听 window.onscroll 事件。

js
// 这种代码简直是性能杀手
window.addEventListener('scroll', () => {
  const scrolled = window.scrollY;
  const header = document.querySelector('.header');
  // 每次滚动一像素,浏览器就要重新计算并渲染,CPU 都在尖叫
  header.style.opacity = 1 - (scrolled / 200);
});

问题: 滚动事件每秒触发几十甚至上百次。如果你的 JS 稍微复杂一点,页面就会掉帧、卡顿(Jank)。为了解决这个问题,开发者发明了“防抖(Debounce)”和“节流(Throttle)”,但这只是止痛药,治标不治本。

第二阶段:工业革命 (Intersection Observer API)

大约 2016 - 2020 年

浏览器厂商终于看不下去了,推出了 Intersection Observer(交叉观察器)。这是一个高效的 JS API。

  • 做法: 浏览器帮你盯着元素,告诉 JS:“嘿,这个图片进入屏幕了!”然后 JS 给它加个类名 .animate
  • 进步: 性能大大提升,因为不再需要实时监听滚动位置。
  • 局限: 它只能做开关(触发动画开始/结束),很难做连续映射(比如:滚动条走 10%,动画就走 10%;滚动条倒回去,动画也倒回去)。

第三阶段:现代文明 (CSS Scroll-driven Animations)

2023 年至今 (Chrome 115+ 开始支持)

这就是让你惊叹的 animation-timeline

  • 核心理念: 时间不再是以“秒”为单位,而是以“像素”为单位。
  • 革命性: 以前 CSS 动画是 0s -> 3s;现在 CSS 动画是 Scroll Top (0px) -> Scroll Bottom (1000px)

实战演示:两种模式

CSS 滚动驱动动画主要分为两种玩法,非常直观:

1. scroll() - 宏观视角 (阅读进度条)

盯着整个滚动容器(通常是页面本身)。

场景: 页面顶部的红色进度条,随着你读完全文,它从左走到右。

css
/* 定义动画:从宽 0 到宽 100% */
@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

.progress-bar {
  /* 绑定动画 */
  animation: grow linear;
  /* 魔法所在:动画的时间轴 = 最近的滚动容器 */
  animation-timeline: scroll(); 
}
2. view() - 微观视角 (元素显现)

盯着元素自己。它关注的是“我自己有没有进入屏幕”。

场景: 图片默认是透明的,当它从屏幕底部露头时慢慢变实,滚出屏幕顶部时又变透明。

css
@keyframes fade-in-out {
  0% { opacity: 0; transform: translateY(50px); }
  50% { opacity: 1; transform: translateY(0); } /* 在屏幕中间时完全显示 */
  100% { opacity: 0; transform: translateY(-50px); }
}

.image {
  animation: fade-in-out linear;
  /* 魔法所在:当这个元素进入视口(view)时开始,离开时结束 */
  animation-timeline: view();
  /* 精确控制:从进入屏幕 10% 处开始,到离开屏幕 10% 处结束 */
  animation-range: entry 10% exit 10%;
}

“F8” 大法

这是 Chrome 官方提供的快捷键,专门用来暂停脚本执行。

  • 适用场景:你想暂停界面,但不想改代码。
  • 操作步骤
    1. 打开开发者工具(F12)。
    2. 切换到 Sources (源代码) 面板。
    3. 把鼠标移到你想触发的界面上(比如悬停让菜单显示)。
    4. 按下键盘上的 F8(或者 Ctrl + \)。
    5. 效果:浏览器会立刻进入“暂停”状态(Paused in debugger),此时你可以随意去 Elements 面板检查那个原本会消失的元素了。

4. 逻辑控制的新星:Style Queries (样式查询)

第一阶段:洪荒时代 —— 只有“全局”没有“局部”

(Before 2010s)

在这个阶段,CSS 是**“一视同仁”**的。

  • Media Queries (@media):只能查询浏览器窗口(视口)的宽度。
  • 痛点: 你写了一个组件(比如卡片),你想让它在“变宽”时改变布局。但你只能问浏览器:“窗口有没有变宽?”而不能问组件:“爸爸(父容器)有没有变宽?”

第二阶段:觉醒年代 —— 容器查询 (Container Queries)

(2020 - 2022)

这是大家最熟悉的历史。开发者苦苦哀求了 10 年,浏览器终于推出了 @container

  • 核心能力: 尺寸查询 (Size Queries)
  • 语法: @container (min-width: 500px) { ... }
  • 意义: 组件终于可以根据父容器的宽度来改变样式了。这解决了响应式开发 80% 的痛点。

第三阶段:逻辑革命 —— 样式查询 (Style Queries)

(2023 - 至今)

就在大家为“尺寸查询”欢呼时,规范制定者(CSS WG)突然意识到一个问题:

“既然我们可以查询父容器的宽度,为什么不能顺便查询父容器的样式值呢?”

于是,Style Queries 诞生了。

它不再关心“父容器有多宽”,而是关心“父容器的变量是多少”。这实际上就是在 CSS 里写 if (var == value)

这是 Container Queries (容器查询) 的进阶版。

现状: 我们知道 @container (min-width: 300px) 可以根据宽度变样式。

未来(Style Queries): 我们可以根据 CSS 变量的值 来变样式。这是 CSS 里的 if/else

css
/* 容器定义 */
.card {
  container-name: my-card;
}

/* 这是一个逻辑开关 */
.card.featured {
  --is-featured: true; 
}

/* 样式查询:如果 --is-featured 变量等于 true,则执行下面的 CSS */
@container my-card style(--is-featured: true) {
  .title {
    color: gold;
    font-size: 2rem;
  }
  .bg {
    background: linear-gradient(to right, red, gold);
  }
}

底蕴价值: 这实现了 样式与结构的分离。React/Vue 组件只需要控制父容器的一个变量开关,CSS 内部就会自动处理所有子元素的复杂变化逻辑。

巨大的挑战

虽然理念很先进,但它目前面临两个巨大的挑战:

1. 循环依赖的死结 (The Circular Dependency Loop)

这是规范制定者最头疼的问题。

设想: 我想查询 background-color

css
@container style(background-color: red) {
  div { background-color: blue; } 
}

BUG: 如果子元素变蓝会导致父元素变红,父元素变红又触发查询让子元素变蓝……这就死循环了。 现状: 为了避免这个问题,目前的浏览器(主要是 Chrome)只支持查询 CSS 变量(Custom Properties),比如 style(--foo: bar)不支持查询常规属性,比如 style(width: 100px)style(color: red)

2. 兼容性 (Browser Support)

截至目前(2024年),只有 Chrome / Edge (Blink内核) 完全支持。Safari 和 Firefox 还在观望或开发中。

评论

评论加载中……