首页 / 文章

安全使用现代CSS:锚点、网格轨道、作用域与light-dark()函数

了解锚点定位、弹出框、网格通道砌体、视图过渡、light-dark() 以及 @scope 如何替代 JavaScript 库,以及如何为每种方案准备备用方案。

发布者:REACTAPP.TOP阅读约 7 分钟1605 词

来源: CSS Can Already Do This. Developers Are Still Scared to Ship It.

许多前端打包文件仍包含专门用于确定工具提示位置的工具库,例如 Floating UI 或其前身 Popper,还有将内容以 Pinterest 风格网格排列的工具库。如今浏览器已能原生实现这些功能,以及许多过去需要 JavaScript 才能完成的操作,但仍有少数团队没有移除这些依赖。本指南介绍了五种可替代常见库代码的 CSS 特性,解释了每种特性的工作原理,并展示了如何在不必担心浏览器兼容性问题的情况下采用它们。

既被需要又遭回避

开发者调查很好地反映了这种矛盾状况。在2026年的CSS现状调查结果中,锚点定位被选为开发者最期待的新功能,取代了现已被广泛使用、不再具有新颖感的:has()。同一项调查还询问了人们因不信任浏览器支持而刻意避免哪些功能,锚点定位同样位居榜首。缩小这一差距的关键在于通过特性查询来采用相关功能。调查数据和支持状况变化迅速,因此应将其视为阶段性信息,在依赖任何功能之前务必查看最新的兼容性数据。

使用锚点定位而非工具提示库

JavaScript 方法通过同时测量触发元素和浮动元素的位置,并在每次滚动或窗口大小改变时重新计算位置,从而将浮动元素附着在触发元素上。而锚点定位则将这种关系交由 CSS 处理。

在下文中,触发元素会声明一个名称,菜单则会依据该名称定位自身。最后的声明则替代了原有库中的碰撞检测功能:

.trigger {
  anchor-name: --menu-trigger;
}
.menu {
  position: fixed;
  position-anchor: --menu-trigger;
  top: anchor(bottom);
  left: anchor(center);
  position-try-fallbacks: flip-block, flip-inline;
}

每个部分都有特定的作用:

  • anchor-name 为某个元素提供一个虚线标识,其他元素可依据该标识进行定位。
  • position-anchor 告诉已定位的元素应跟随哪个带名称的锚点。
  • anchor() 直接从该元素读取坐标,例如锚点的下边缘或水平中心点。
  • position-try-fallbacks 列出了当元素超出视口时浏览器会尝试的替代方案;flip-block 和 flip-inline 可以分别对元素进行垂直或水平翻转。
  • 与 popover 属性结合使用

    popover 属性虽然已被内置但往往被忽视,它用于控制下拉菜单的行为:带有 popovertarget 属性的按钮可以切换具有对应 id 的元素,无需任何脚本:

    <button popovertarget="menu">Options</button>
    <div id="menu" popover anchor-name-target="--menu-trigger">...</div>
    

    使用此代码片段时需谨慎:anchor-name-target并非标准HTML属性,因此不可直接复制使用。应如上所示,通过CSS中的anchor-name和position-anchor将弹出框与其锚点关联。某些浏览器还会将触发弹出框的按钮视为通过popovertarget打开的弹出框的隐式锚点;在依赖此功能前请查阅最新文档。

    popover本身具备顶层渲染功能,因此菜单会显示在所有内容之上,无需处理应用内的z-index冲突;用户点击页面其他区域或按Esc键时即可轻松关闭弹出框,且在打开和关闭过程中会有合理的焦点管理机制。它不会像模态dialog那样锁定焦点,因此若需要真正的模态内容,请使用dialog。

    如需更全面地了解那些让 npm 包变得不再必要的浏览器 API,可参阅我们关于原生浏览器 API 替代热门 npm 包的综述。

    带网格列的原生 Masonry 布局

    自 2017 年 CSS Grid 进入浏览器以来,Masonry 布局仍需依赖 JavaScript,这也是 Masonry 库及类似包被安装数百万次的原因。但这种情况正在改变。以下规则即可实现完整布局:

    .gallery {
      display: grid-lanes;
      gap: 10px;
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    }
    

    每张卡片都会被放置到当前空间最大的列中,这样各项就能紧密排列而不会出现参差不齐的间隙。该特性没有使用ResizeObserver,也不会在图片加载完成后通过脚本重新调整列的布局。浏览器厂商之间为这一语法标准进行了多年的争论,苹果和谷歌各自支持不同的方案,最终display: grid-lanes作为折中方案被采用。

    根据撰写本文时的支持情况,Safari率先实现了该功能,而Chrome和Firefox则将其置于实验性选项中。这也就解释了为何只有少数网站使用它,以及现在为何值得去实现它。应将grid-lanes放在@supports (display: grid-lanes)块中,并以普通网格布局作为备用方案。

    视图过渡:需格外谨慎

    在此情况下确实需要谨慎。虽然存在视图过渡 API 的支持,但其实现程度参差不齐,这使得它与锚点定位一同成为最常被避免使用的功能,这种犹豫是合理的。

    其基本机制很简单:将 DOM 更新操作包裹在 document.startViewTransition() 中,浏览器便会截取旧状态和新状态并实现两者之间的渐变过渡,无需任何动画库,也无需手动记录前后状态。以下的伪元素可用于控制这种默认的过渡效果,此处将其时长缩短至0.3秒:

    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: 0.3s;
    }
    

    目前它被视作一种渐进式增强方案。缺少相应 API 的浏览器会直接跳转到新状态,因此不会出现任何问题。这使其适用于页面切换或列表重排等非关键场景。在脚本中,调用 document.startViewTransition 之前需先确认该函数存在,否则直接进行更新即可。

    使用 light-dark() 实现明暗主题统一管理

    典型的深色模式会维护两个完整的颜色系统,一个位于 prefers-color-scheme: dark 媒体查询规则内,另一个则在外部。而 light-dark() 函数可将每对颜色系统合并为单一声明。

    下面的代码片段首先通过 color-scheme 令文档启用两种主题模式(这是 light-dark() 能正常工作的前提),随后为每个属性并列设置明色和暗色值:

    :root {
      color-scheme: light dark;
    }
    
    body {
      background: light-dark(#ffffff, #1a1a1a);
      color: light-dark(#111111, #eeeeee);
    }
    

    一旦设置了 color-scheme,每次调用 light-dark() 都会根据用户的系统设置返回正确的值,这样就无需在样式表中重复编写媒体查询。关于实现主题化的其他方法,请参阅我们关于深色模式技术的对比文章。

    使用 @scope 限制样式范围

    CSS-in-JS 或严格的 BEM 命名规则的主要出发点在于解决特异性泄漏问题:原本为某个组件设计的 .card 规则,可能会影响到其他页面中嵌套的、与原组件无关的 .card 元素。@scope 能够原生地将样式限制在某个子树范围内。

    在下面的规则中,段落样式仅适用于 .card 内部的元素,一旦遇到 .card-nested 元素则停止生效:

    @scope (.card) to (.card-nested) {
      p {
        color: var(--card-text);
      }
    }
    

    第一个选择器确定了作用域的根节点。to子句设定了下限,从而形成一段空白区域,在该区域内嵌套组件可以自行管理样式,而无需继承父组件的样式。这一切都不需要构建步骤、生成的类名或运行时的JavaScript代码。它虽然无法取代JS中的CSS所具备的所有功能,比如基于组件属性计算样式,但却消除了团队采用JS中CSS这一方案的主要理由之一。

    浏览器即生态系统

    如今,这五种功能至少已在一种真实的浏览器中实现,而且它们都不依赖于React、打包器插件或任何框架版本。CSS的功能范围与团队实际使用情况之间的差距,很大程度上源于习惯:十年来,解决UI问题往往意味着要安装某个软件包。

    选择项目中因某个问题而使用 JavaScript 变通方案的位置,在其前面加上 @supports,然后看看会有什么功能出问题。通常出现的问题会比预期少。

    常见问题

    锚点定位功能已可投入生产使用了吗?

    该功能已经正式发布,如今可以通过 @supports 检查来使用。它被列入最需谨慎使用的功能列表,是因为担心不同浏览器间的兼容性差异,并非该功能本身有缺陷。将其与备用方案(如更简单的静态定位方式)结合使用,才是恰当的应对措施。

    还需要使用网格布局库吗?

    对于新项目来说,只要将 display: grid-lanes 放在 @supports 块中,并以普通网格布局作为备用方案,通常就不需要了。而对于那些对浏览器兼容性要求极高的现有生产环境布局,则仍需继续使用该库一段时间。

    您应该首先采用哪个功能?

    light-dark()——只需为每个颜色值进行一行代码修改,在撰写本文时它在这五种功能中得到了最广泛的支持,而且无需担心布局问题即可删除大量重复的深色模式CSS代码。

    关键要点

    • 将每个功能视为渐进式增强方案:在支持的情况下使用原生行为,否则采用现有的解决方案。
    • anchor positioning结合popover即可满足大多数工具提示和下拉菜单的需求,但若需要真正的模态框,则应在CSS中设置锚点并使用dialog。
    • grid-lanes与视图过渡功能目前尚不成熟,应通过@supports或运行时检查来决定是否使用。
    • light-dark()与@scope能立即简化代码,且风险最低。