CSS简化JS

desc: "这里讲的就是所有CSS新增的属性能取代原有的冗长的JS额外开发"


desc: “这里讲的就是所有CSS新增的属性能取代原有的冗长的JS额外开发”

CSS简化JS

表单体验优化

这是**表单体验(UX)**的救星。

你用了 HTML5 的 requiredtype="email"

css
input:invalid { border: 2px solid red; }

结果页面一加载,所有空的输入框瞬间全红了!用户还没填呢就被报错,体验极差。以前只能写 JS 监听 blur 事件加类名。

:invalid:只要数据不对,立刻生效(不管用户动没动)。

:user-invalid只有当用户和输入框交互过(打字、甚至提交过)之后,如果数据还是不对,才生效。

css
/* 只有当用户填错、或者尝试提交空表单后,才变红 */
input:user-invalid {
  border-color: red;
  background-color: #fff0f0;
}

/* 输入正确时给个绿色勾勾 */
input:user-valid {
  border-color: green;
}

底蕴价值: 这把交互状态逻辑从 JS 移交给了浏览器。浏览器比 JS 更懂“用户什么时候完成了输入”。

锚点定位优化

解决“固定头部的遮挡”:scroll-padding

这是每个做过“锚点定位(Anchor Links)”的人都遇到过的 Bug。

页面顶部有一个 position: fixed 的导航栏(比如高 60px)。当你点击目录里的“跳转到第一章”时,浏览器会把“第一章”的标题卷到视口最顶端——结果正好被导航栏挡住了!

以前的 Hack: 给每个标题加 padding-top: 60px; margin-top: -60px;。这种代码简直是维护的噩梦。

现代解法: 告诉滚动容器(通常是 html),它的“可视区域”上面要留一点空隙。

css
html {
  /* 告诉浏览器:滚到锚点时,往下多留 80px 的空间 */
  /* 完美避开 60px 的固定导航栏 */
  scroll-padding-top: 80px; 
  /* 顺便加上平滑滚动 */
  scroll-behavior: smooth;
}

底蕴价值: 这是滚动容器的模型修正。它从根本上定义了“什么是可见区域”,而不是在每个子元素上打补丁。

轮播图新解

scroll-snap (滚动捕捉) 这是用来杀掉 JS 轮播插件 (Swiper.js, Slick) 的属性。

痛点: 做一个全屏滑动的 H5 页面,或者横向滚动的商品列表。你需要 JS 监听 touchend,计算滑动速度,计算吸附位置,写动画……几百行代码。

现代解法: 像磁铁一样吸附。

css
.carousel {
  overflow-x: auto;
  display: flex;
  /* 1. 父容器设置:x 轴滚动,mandatory 表示必须吸附(手指松开时一定会停在某一张图上) */
  scroll-snap-type: x mandatory; 
}
.slide {
  flex: 0 0 100%;
  /* 2. 子元素设置:center 表示这就这张图的中心会对齐父容器的中心 */
  scroll-snap-align: center; 
  /* 3. 甚至可以控制停止时的硬度 */
  scroll-snap-stop: always; 
}

底蕴价值: 它提供了原生级的物理手感。JS 模拟的滚动往往会有“粘滞感”或掉帧,而 scroll-snap 是浏览器底层的滚动机制,流畅度是满帧的。

CSS 计数器

这是CSS自带的计数器,是CSS的数据存储能力

你写了一篇长文,有 h1, h2。你想自动给它们加上编号:1. 第一章1.1 第一节1.2 第二节

以前的做法: 后端渲染时算好写在 HTML 里,或者用 JS 遍历 DOM 加上去。

现代解法: CSS 自己就能数数。

css
/* 父容器:初始化计数器,起名叫 section */
body {
  counter-reset: section; 
}
h1 {
  /* 每次遇到 h1,section 计数器 +1 */
  counter-increment: section; 
}
h1::before {
  /* 把当前的数字显示出来 */
  content: "第" counter(section) "章 ";
  color: blue;
}
/* 甚至支持嵌套计数!(1.1, 1.2...) */
h2 {
  counter-increment: subsection;
}

还有我们学过 counter-reset (重置) 和 counter-increment (增加)。如果我想强行修改计数器的值也可以 直接赋值。

css
li.special {
  /* 核心:把计数器直接设为 10,不管前面数到了几 */
  counter-set: section 10;
}

底蕴价值: 这是内容与样式的分离。编号属于表现层,不应该硬编码在 HTML 里。如果我们要把章节顺序调换,CSS 会自动重算,完全不需要改 HTML。

智能焦点管理

:focus-visible这是**交互体验(UX)与无障碍(A11y)**的完美平衡。

  • 痛点:
    • 为了美观: 设计师讨厌点击按钮时出现的那个“丑陋的蓝色外框”,让你写 outline: none
    • 为了可用性: 键盘用户(按 Tab 键导航的人)如果没有那个框,完全不知道焦点在哪里。
  • 以前的矛盾: 这是一个死局。
  • 现代解法: :focus-visible
css
/* 全局禁用默认的丑框 */
:focus {
  outline: none;
}

/* 核心:只有当浏览器认为"用户真的需要看焦点"时(比如按键盘 Tab 时),才显示框 */
/* 如果用户是用鼠标点击,浏览器知道他在看哪里,就不显示框 */
:focus-visible {
  outline: 2px solid blue;
  outline-offset: 2px;
}

底蕴价值: 这体现了你对用户行为的精细控制。你不再是一刀切地去掉样式,而是根据输入设备(鼠标 vs 键盘)提供不同的反馈。

瀑布流布局的未来

grid-template-rows: masonry (实验性)

这是布局领域的圣杯。虽然目前只有 Firefox 支持(且需开启配置),但作为架构师,你必须知道它即将到来。

痛点: Pinterest(瀑布流)布局。

  • Grid 的问题: Grid 默认是一行一行对齐的。如果第一行有个很高的卡片,第二行的卡片会被顶下去,留下巨大的空白。

  • 以前的做法: 必须用 Masonry.js 这种重型 JS 库来绝对定位每一个卡片。

  • 现代解法: 原生瀑布流。

css
.container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* 核心:行高不再固定,而是像砌墙一样自动填补空隙 */
  grid-template-rows: masonry;
  /* 让间距均匀 */
  gap: 20px;
}

底蕴价值: 这标志着 CSS 布局引擎的完全体。一旦普及,成千上万行的 JS 布局代码将被这一行 CSS 替代。

穿透点击

pointer-events: none这是穿透点击的神器。

场景: 你想在按钮上覆盖一层半透明的漂亮光晕,或者加一个自定义的鼠标跟随特效(Custom Cursor)。但是这个特效层盖在按钮上面,导致按钮点不到了!现在就让这个元素在鼠标眼里“不存在”。

css
.overlay-effect {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  
  /* 核心:鼠标事件直接穿透我,传给下面的人 */
  /* 我可见,但我不可触 */
  pointer-events: none; 
}

/* 进阶玩法:禁用按钮的点击,但不改样式(比如提交中状态) */
.btn.is-loading {
  pointer-events: none; /* 用户怎么狂点都没用 */
  opacity: 0.7;
}

复制粘贴的优化:user-select: all

这是开发者体验 (DX)用户体验 (UX) 的细节。

你的网站上展示了一串 API Key,或者一段 代码命令 (npm install ...)。用户想复制,还得用鼠标小心翼翼地从头拖到尾。如果手抖少选了一个字母,运行就报错。

现代解法: 一点即全选。

css
code, .api-key {
  /* 核心:用户只要点一下,浏览器自动选中所有文本 */
  user-select: all; 
  /* 配合一下样式,暗示它是可复制的 */
  cursor: pointer;
  background: #f4f4f4;
}

/* 反向用法:防止用户复制按钮文字或图标 */
button, .icon {
  user-select: none; /* 防止双击按钮时蓝底选中文本,很难看 */
}

布局结构解放

display: contents这是一个 “幽灵属性”,它极大地解放了 HTML 结构。

为了语义化或组件化,你给某个部分包了一层 div<section>。结果:这层 wrapper 破坏了外层的 FlexGrid 布局。因为 Flex/Grid 只认直接子元素以前的做法: 被迫把这层 wrapper 删掉,或者把 wrapper 也设成 display: flex 并传递属性(非常繁琐)。现在可以直接让这层盒子消失

css
.wrapper {
  /* 核心:
     1. 浏览器渲染时,直接忽略我(.wrapper)。
     2. 把我的子元素,当做是我父元素的直接子元素。
     3. 但我的语义(如 section, role)依然保留,不影响 SEO 和无障碍。
  */
  display: contents;
}

底蕴价值: 它实现了 DOM 结构与视觉布局的完全解耦。你终于可以在 HTML 里写无数层语义化的标签,而在 CSS 里像它们不存在一样进行扁平化布局。

内容的投影

filter: drop-shadow()

你一定用过 box-shadow,但你可能不知道它其实是个“傻瓜”。

你有一张透明背景的 PNG 图片(比如一个不规则的 Logo,或者一个三角形的气泡)。你用 box-shadow,结果影子是一个死板的矩形框,非常丑。

现代解法: 基于像素 alpha 通道的投影。

css
.logo {
  /* 错误示范:这会给图片的外框加阴影 */
  /* box-shadow: 0 10px 10px rgba(0,0,0,0.5); */

  /* 正确示范:这会检测图片的透明度,只给不透明的部分加阴影 */
  filter: drop-shadow(0 10px 10px rgba(0,0,0,0.5));
}

底蕴价值: box-shadow 是给盒子用的,drop-shadow 是给内容用的。利用这个特性,你甚至可以用纯 CSS 给 SVG 图标或镂空文字加边框。

CSS 的原初变量

currentColor (当前颜色) 很多新手知道 CSS 变量 (--var), 但忽略了这个 CSS 最古老的变量

场景: 你做了一个按钮,里面有一个图标 (SVG) 和一行文字。痛点: 鼠标悬停 (hover) 时,文字变色了,但图标没变。你不得不写两行代码:color: blue; fill: blue;

现代解法: 让图标的颜色自动“继承”文字的颜色。

css
.button {
  color: #333;
  border: 1px solid currentColor; /* 边框色自动等于文字色 */
}

.button svg {
  /* 核心:我不指定具体颜色,我只跟随父级的文字颜色 */
  fill: currentColor; 
}

.button:hover {
  /* 只需要改这一处! */
  /* 文字、边框、图标 会同时变成红色 */
  color: red; 
}

底蕴价值: 这是 DRY 原则 (Don’t Repeat Yourself) 在 CSS 中的极致体现。组件越复杂,currentColor 省下的代码就越多。它让你的组件具有极强的**“上下文适应性”**。

移动端悬停粘滞优化

@media (hover: hover)这是区分**“PC 端思维”“全平台思维”**的重要标志。

你给按钮写了 :hover { transform: scale(1.1); }Bug: 在手机上,你点击按钮后,按钮会一直保持放大状态(因为手机没有“鼠标移出”的概念,焦点还停留在那里)。如果你点的是跳转链接,返回上一页时,那个按钮还是放大的。现在应该是只有当**设备真的支持悬停(比如有鼠标)**时,才应用 Hover 效果。

css
/* 只有当输入设备支持 hover 时(比如鼠标),才执行这里的 CSS */
@media (hover: hover) {
  button:hover {
    background-color: blue;
    transform: translateY(-2px);
  }
}
/* 手机用户根本不会加载这段代码,完美解决了"粘滞"问题 */

底蕴价值: 这是输入模态 (Input Modality) 的适配。响应式不仅仅是查屏幕宽度 (min-width),更是查设备能力。

JS绘制CSS

这是 CSS 历史上最大的权限开放。以前,我们想要一个“波浪线边框”,如果 CSS 没提供 border-style: wave,我们就只能用图片。

现在,Houdini 允许你用 JavaScript 写一个绘图算法,然后把它注册成一个 CSS 属性值

步骤 1:写一段 JS (Worklet)

js
// paint-worklet.js
registerPaint('smooth-corners', class {
  paint(ctx, size, properties) {
    // 这里使用类似 Canvas 的 API 画画
    // 画一个超级圆滑的矩形(Squircle,类似 iOS 图标)
    ctx.fillStyle = 'black';
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(size.width, size.height);
    // ... 复杂的贝塞尔曲线计算 ...
    ctx.fill();
  }
});

步骤 2:在 CSS 里调用它

css
.card {
  /* 核心:本来这里只能填 url(image.png) */
  /* 现在我们可以填 paint(我们刚注册的名字) */
  background-image: paint(smooth-corners);
  /* 甚至可以传参数进去! */
  --corner-radius: 20;
}

底蕴价值: 这打破了 CSS 的物理限制。你不再等待 W3C 发布新属性,你自己就是规范制定者。你可以自己发明 background: paint(star-sky)border-image: paint(chat-bubble).

更准确选择器

: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 树深处的特定元素。

移动端手势控制

touch-action这是区分 Web AppNative App 手感的关键。

作用
auto默认值,浏览器根据情况决定允许哪些手势
none禁止所有手势(不会触发滚动、缩放,需要你用 JS 自己处理)
pan-x允许水平方向滚动,禁用纵向滚动
pan-y允许垂直方向滚动,禁用水平方向滚动
pan-x pan-y同时允许水平和垂直滚动
manipulation允许滚动和缩放,但禁用双击缩放(常用于移动端避免双击页面放大)

场景: 你做了一个“地图”组件,或者一个“左滑删除”的列表项。

  • 痛点: 当用户手指在地图上拖动时,浏览器以为用户想滚动页面,于是页面跟着动了,地图没动。体验极差。
  • 以前的做法: event.preventDefault()

现代解法: 声明触摸行为。

css
.map-container {
  /* 核心:告诉浏览器,这个区域禁止浏览器处理任何手势(滚动、缩放) */
  /* 这块地盘归我 JS 管了 */
  touch-action: none;
}
.horizontal-scroll-list {
  /* 只允许横向滚动(由浏览器处理),禁止纵向滚动 */
  /* 这样用户斜着滑的时候,页面不会乱动 */
  touch-action: pan-x; 
}

底蕴价值: 这是对交互所有权的申明。它让你的 H5 页面操作起来像原生应用一样跟手,没有浏览器的干扰。

原生滚动条美化

scrollbar-color & scrollbar-width

你一定见过 -webkit-scrollbar 这种私有属性,但那是非标准的。现在,W3C 标准来了。

  • 默认滚动条太宽、太丑,和深色模式网站格格不入。
  • 以前的 -webkit- 写法非常冗长,且 Firefox 不支持。

scrollbar-width: 控制滚动条的粗细。

  • 值:auto (默认)、thin (变细)、none (隐藏但可滚动)。

scrollbar-color: 同时设置滚动滑块(thumb)和滚动条轨道(track)的颜色。

  • 语法:scrollbar-color: [滑块颜色] [轨道颜色];
css
.scroll-container {
  /* 1. 宽度:thin (变细) 或 none (隐藏但可滚动) */
  scrollbar-width: thin;
  /* 2. 颜色:滑块颜色 轨道颜色 */
  scrollbar-color: #888 #f1f1f1;
}

传统的 WebKit 私有属性

伪元素作用
::-webkit-scrollbar整个滚动条的容器(可设置宽度/高度)
::-webkit-scrollbar-thumb滚动的滑块部分
::-webkit-scrollbar-track滚动条的外层轨道
::-webkit-scrollbar-button两端的箭头按钮
::-webkit-scrollbar-corner横向与纵向滚动条交汇的直角处
css
/* 整个滚动条 */
::-webkit-scrollbar {
  width: 10px;        /* 竖向滚动条宽度 */
  height: 10px;       /* 横向滚动条高度 */
}
/* 滚动条轨道(背景) */
::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 10px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 10px;
}
/* 鼠标悬停时 */
::-webkit-scrollbar-thumb:hover {
  background: #555;
}
/* 可选:滚动条两端按钮 */
::-webkit-scrollbar-button {
  display: none; /* 隐藏上下箭头 */
}
/* 4. 上下箭头按钮 */
::-webkit-scrollbar-button {
  display: block;
  background-color: #aaa; /* 必须有颜色 */
  background-repeat: no-repeat;
  background-size: 50%;
  background-position: center;
  height: 12px; /* 必须有高度 */
}
/* 5. 专门设置向上箭头 */
::-webkit-scrollbar-button:single-button:vertical:decrement {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23555'><polygon points='50,0 0,100 100,100'/></svg>");
}
/* 6. 专门设置向下箭头 */
::-webkit-scrollbar-button:single-button:vertical:increment {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23555'><polygon points='0,0 100,0 50,100'/></svg>");
}

交互增强属性

除了视觉样式,现代 CSS 还提供了控制滚动行为的属性:

  • scroll-behavior: 设置滚动是“平滑(smooth)”还是“瞬间(auto)”。
  • overscroll-behavior: 控制滚动到边界时的连锁反应(防止“滚动链”导致父级容器跟着动)。
  • scrollbar-gutter: 预留滚动条出现的位置,防止滚动条出现时页面内容发生抖动(Layout Shift)。

scroll-behavior:滚动丝滑度

这个属性决定了当用户点击锚点链接或使用 JS 触发滚动时,页面是“瞬移”还是“平滑滑动”。

常用值

  • auto (默认):立即跳转。
  • smooth:平滑动画过渡。

应用场景:常用于“回到顶部”按钮或页面内导航。

css
html {
  scroll-behavior: smooth;
}

overscroll-behavior:切断滚动链

这是解决“滚动穿透”或“滚动链”问题的神技。

当你滚动一个弹窗(Modal)到底部时,如果继续滚动,背后的父级页面也会跟着滚动。这通常不是我们想要的。

常用值

  • auto (默认):滚动到边界后继续传递给父容器。
  • contain:阻断滚动传递。滚动到边界时,浏览器会显示回弹效果,但父容器不会动。
  • none:完全禁用回弹和滚动传递。

应用场景:侧边栏菜单、弹出层、下拉刷新组件。

scrollbar-gutter:页面防抖消除

你的页面内容有时候短(没滚动条),有时候长(有滚动条)。

当用户点击“加载更多”时,滚动条突然出现,导致整个页面内容向左挤了 15px。这种视觉上的“闪烁”非常廉价。

以前的 Hack: html { overflow-y: scroll; }(强制永远显示滚动条),但这在没内容时很难看。

现在使用scrollbar-gutter:消灭页面“抖动”

这是一个专门解决**布局偏移(Layout Shift)**的现代属性。

通常情况下,内容多了会触发滚动条。滚动条一旦出现,会占据页面的宽度(通常是 10px-15px),导致页面内容向左“弹”一下。

  • auto:默认行为,只有溢出时才腾出空间。
  • stable:即使内容没溢出,也预留出滚动条的那条窄槽。
  • stable both-edges:左右两边都预留空间,保持内容居中对齐。

表现为看不见的padding属性!image-20260207151033199

css
html {
  /* 核心:
     不管有没有滚动条,浏览器都会在右边预留出滚动条的宽度。
     这样内容区永远是稳定的,不会左右乱跳。
  */
  scrollbar-gutter: stable;
  /* 进阶:stable both-edges 可以让两边都留白,实现绝对对称 */
}

列表的优化样式::marker

::marker这是专门用来修整 <ul><ol> 的。

你想改列表前面的“小圆点”或者“数字”的颜色、大小。以前需要设置list-style: none,然后用 ::before 手画一个圆点定位上去。非常麻烦,而且容易对不齐。现在可以直接操作标记伪元素了!

css
li::marker {
  /* 核心:只改变前面的圆点/数字,不影响内容文字 */
  color: red;
  font-size: 1.5em;
  content: "🚀"; /* 甚至可以直接换成 Emoji! */
}
/* 也可以针对有序列表 */
ol li::marker {
  content: "(" counter(list-item) ") ";
  color: orange;
}

底蕴价值: 这是伪元素 (Pseudo-elements) 家族的重要成员。它让 HTML 的语义化标签(<li>)变得好用,而不需要为了样式去用 <div> 模拟列表。

image-20260211185041169

上传按钮样式优化

::file-selector-button

这是前端开发历史上被吐槽最狠的组件之一:<input type="file">。浏览器默认的“选择文件”按钮丑出天际,而且还改不了样式!以前直接把 input 设为 opacity: 0,然后手写一个 div 盖在上面,或者用 <label> 触发点击。非常麻烦。现在可以 直接伪元素美化。

css
/* 选中那个丑丑的默认按钮 */
input[type="file"]::file-selector-button {
  height: 40px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 0 15px;
  margin-right: 10px;
  cursor: pointer;
  
  /* 甚至可以加 hover 效果 */
  transition: background 0.3s;
}

input[type="file"]::file-selector-button:hover {
  background: #0056b3;
}

底蕴价值: 这代表了 Shadow DOM 的开放。浏览器终于允许我们去修饰那些封闭在组件内部的原生部件了。

image-20260211185320668

焦点内查focus-within

:focus-within 这是表单交互设计中最有用的伪类,没有之一。

你有一个搜索框组件:外层是一个 div(有边框、阴影、图标),里面放了一个透明的 <input>。当用户点击内部的 <input> 时,你想让外层的 div 变色(比如边框变蓝、阴影变大)。

以前的做法: 必须写 JS 监听 input 的 focus 事件,给父级 div 加类名 .is-focused

现代解法: CSS 自己就能“看到”里面有没有人聚焦。

css
.search-container {
  border: 1px solid #ccc;
  padding: 10px;
  border-radius: 8px;
  transition: all 0.3s;
}

/* 核心:
   当 .search-container 内部的"任何子元素"(比如 input)获得焦点时,
   触发这个样式。
*/
.search-container:focus-within {
  border-color: #007bff;
  box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2);
}

底蕴价值: 这是 状态冒泡 (State Bubbling) 的 CSS 版本。它让父容器能够感知子孙元素的状态,彻底消灭了“为了高亮父级而写 JS”的样板代码。

image-20260211185548162

这里也可以使用父类选择器:

css
/* 核心:
   选择 .search-container,前提是它包含(has)一个获得焦点的后代(*:focus)
*/
.search-container:has(*:focus) {
    border-color: #007bff;
    box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2);
}

俄罗斯方块布局

grid-auto-flow: dense (紧密填补)

这是 CSS Grid 布局中最有趣的算法。

场景: 你做一个图片墙(Photo Gallery),图片有大有小。

痛点:

  • 第一行放了 3 张小图。
  • 第二行第一张是个大图(占两列)。
  • 结果: 浏览器发现第一行最后剩一个空位,塞不下大图,于是把大图挤到第二行,第一行就留下了一个丑陋的空白窟窿

现代解法: 告诉浏览器:“后面有塞得进去的小图吗?如果有,先拿上来填空!”

css
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  
  /* 核心:
     dense: 启用"紧密堆积"算法。
     浏览器会像玩俄罗斯方块一样,自动把后面的小元素
     挪上来填补前面的空隙。
  */
  grid-auto-flow: dense; 
}

.big-item {
  grid-column: span 2; /* 这是一个大块头 */
}

底蕴价值: 这是 DOM 顺序与视觉顺序的解耦。虽然 HTML 里小图在后面,但视觉上它跑到了前面。注意:这对无障碍(Tab 键顺序)可能会有影响,需谨慎使用。

image-20260211193011509

变为:

image-20260211193030312

原生遮罩层

::backdrop 这是配合我们之前讲过的 popover<dialog> 使用的特权伪元素

你做了一个弹窗(Modal)。你需要给它背后加一个半透明的黑色遮罩层。以前的做法: 在 HTML 里多写一个 <div class="overlay"></div>,然后给它写一堆 position: fixed; top: 0...。现在浏览器送你一个免费的遮罩层。

css
/* 针对原生 <dialog> 或 popover 元素 */
dialog::backdrop, [popover]::backdrop {
  /* 核心:
     这个伪元素自动铺满全屏,且层级位于弹窗之下、页面之上。
     你只需要关心它的长相。
  */
  background-color: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(5px); /* 顺便加个毛玻璃 */
  
  /* 甚至可以做进场动画 */
  animation: fade-in 0.3s;
}

底蕴价值: 这是 Top Layer (顶层图层) 的一部分。浏览器帮你管理了层级关系(z-index),你永远不用担心遮罩层盖住了弹窗,或者弹窗盖不住下面的导航栏。

popover 属性

以前写模态框(Modal)、下拉菜单(Dropdown)或者提示框(Tooltip)是最烦人的。

痛点: 你要处理 z-index(为了不被遮挡设为 9999),要写 JS 监听点击外部关闭(Click Outside),要处理 overflow: hidden 的父级截断问题。 现代解法: 浏览器搞出了一个叫 Top Layer(顶层图层) 的概念。拥有 popover 属性的元素会直接“升天”,脱离文档流,永远在所有元素之上。

代码(HTML 为主,几乎零 CSS/JS):

css
<button popovertarget="my-tooltip">点击我</button>

<div id="my-tooltip" popover>
  我是弹窗!我自动居中,自动置顶,点击空白处自动关闭。
</div>

它解决了什么?

  1. 无需 z-index 它在独立的 Top Layer 层,不管父元素怎么设置 overflow: hidden,它都不会被挡住。
  2. 自带交互: 点击页面其他地方,它自动关闭(这叫“轻量解散” Light Dismiss)。
  3. 无障碍友好: 键盘 Esc 键自动关闭。

inert 属性

这个属性看起来不起眼,但它是无障碍 (Accessibility) 和模态框逻辑的神器。

痛点: 当你打开一个侧边栏或弹窗时,你希望背后的页面完全不可交互(不能点击、不能选中文字、Tab 键切不过去)。

以前的做法:

  1. <body>overflow: hidden
  2. 给遮罩层加 pointer-events: none
  3. 写 JS 监听 keydown 事件,把 Tab 键锁在弹窗里(Focus Trap)。
  4. 给背后的所有元素加 aria-hidden="true"

现代解法: 直接给背后的容器加一个属性。

html
<div id="main-content" inert>
  这里的按钮点不了,文字选不中,屏幕阅读器也会直接忽略。
  就像这块区域"冻结"了一样。
</div>

<style>
  /* 配合 CSS 使用,视觉上也变灰 */
  [inert] {
    opacity: 0.5;
    filter: grayscale(1);
  }
</style>

底蕴价值: inert 是浏览器层面的“交互阻断”。它让开发者能以极低的成本实现完美的模态对话框逻辑,是构建复杂 Web 应用的基础设施。

image-20260212130623211

js
main.setAttribute('inert', '');

锚点的高亮

:target这是提升长文档体验的神器。

用户点击了一个链接 example.com/blog#comment-123。页面自动滚动到了 id="comment-123" 的评论处。

痛点: 页面内容太多,用户虽然滚到了那里,但不知道具体是哪一条评论(因为屏幕里可能有很多条)。现代化可以自动高亮当前 URL 指向的元素。

css
/* 当 URL 的哈希值(#hash)与这个元素的 id 匹配时 */
.comment:target {
  /* 给个黄色背景闪一下,告诉用户:由于你点了链接,我现在在这里! */
  background-color: #fff3cd;
  border: 2px solid #ffeeba;
  /* 配合 scroll-margin-top 避免被吸顶导航遮挡,体验满分 */
  scroll-margin-top: 100px;
}

底蕴价值: 这是 路由状态 (Router State) 与样式的结合。CSS 直接响应 URL 的变化,而不需要 JS 去解析 window.location.hash

overflow-anchor (滚动锚定)

这个属性你可能没听过,但你一定享受过它的好处(或者被它的缺失坑过)。

你在手机上看文章,看到一半时,屏幕上方突然加载出来一张大图

痛点: 因为上面变高了,你正在看的段落被瞬间顶到了下面,你不得不手动滑下去找刚才看到哪了。这叫“滚动跳跃”。

现代解法: 浏览器默认其实已经开启了“滚动锚定”(Scroll Anchoring),它会自动锁住你当前的视线位置。但有时候(比如做聊天软件的底部)我们需要关掉它

css
.chat-log {
  /* auto (默认): 浏览器会自动调整滚动位置,不让用户迷路。
     none: 禁用该功能。
     场景:聊天窗口。如果你在看最新的消息(最底部),
     当上面加载历史消息时,你希望滚动条保持在最底部,
     而不是被上面的新内容顶下去。
  */
  overflow-anchor: none;
}

底蕴价值: 这是 浏览器滚动机制 的底层控制。它保护了用户的“阅读上下文”不被突如其来的 DOM 变化所打断。

resize (调整大小)

你只在 <textarea> 上见过右下角那个“小三角”吗?其实任何元素都可以有。

你做了一个侧边栏 (Sidebar) 和一个 内容区 (Main)

痛点: 用户的屏幕大小不一,有些人嫌侧边栏太宽占地方,有些人嫌太窄看不清字。写 JS 做拖拽调整非常麻烦。 纯 CSS 实现拖拽调整。

css
.sidebar {
  width: 300px;
  min-width: 150px;
  max-width: 500px;
  /* 1. 必须开启 overflow,否则 resize 无效 */
  overflow: auto; 
  /* 2. 核心:允许用户在水平方向上拖拽改变宽度 */
  /* options: horizontal (水平), vertical (垂直), both (双向) */
  resize: horizontal;
  border-right: 1px solid #ccc;
}

底蕴价值: 这是 用户赋权 (User Agency)。与其你在那纠结“侧边栏到底是 250px 好还是 300px 好”,不如把选择权交给用户。

image-20260211194108727

表格展示优化

empty-cells这是 数据可视化 中的强迫症福音。

你做了一个数据报表,有些单元格没有数据(是空的)。即使内容是空的,那个单元格的 边框 (border) 还在。如果你给表格加了背景色,那里就像缺了一块牙,非常难看。

现在:只要没内容,连格子都隐身。

css
table {
  /* 核心:
     show: 默认,空单元格也显示边框。
     hide: 空单元格直接消失(边框、背景都没了),看起来像透明的洞。
  */
  empty-cells: hide;
  
  /* 配合 border-collapse: separate 使用效果最佳 */
  border-collapse: separate; 
  border-spacing: 5px;
}

底蕴价值: 这是 数据呈现的降噪。在密集的信息展示中,去掉无意义的视觉干扰(空格子的边框),能让用户更快地聚焦在有数据的区域。

image-20260211194257323

变为:

image-20260211194307572

还可以对有数据格子特殊处理:

css
/* 有数据的格子稍微好看点 */
td:not(:empty) {
  background-color: #e3f2fd; /* 浅蓝色背景 */
  border-color: #90caf9;     /* 蓝色边框 */
  color: #1565c0;
}

CSS文字雨

image-20260211195138096

text-orientation: upright

这是实现文字雨效果的核心。

普通竖排 (writing-mode: vertical-rl):文字会顺时针旋转 90 度躺着(英文文字是这样,中文仍然是竖直的!)。

直立竖排 (upright): 每一个字母都是正着的,像堆积木一样垂直堆叠。

css
h1 {
  writing-mode: vertical-rl;
  /* 核心:让字母不再躺平,而是直立 */
  /* 只有这样才能做出"代码雨"的效果 */
  text-orientation: upright;
  /* 配合等宽字体,效果拔群 */
  font-family: monospace; 
  letter-spacing: -5px; /* 紧凑一点 */
}

caret-color (光标颜色)

你可以修改输入时光标(那个闪烁的竖线)的颜色,使其适应你的深色/浅色背景。

css
input {
  caret-color: red; /* 把光标改成红色 */
}

:placeholder-shown (检测是否为空)

这个伪类选择器非常适合做 “浮动标签 (Floating Label)” 效果(类似 Material Design,输入框没字时 Label 在里面,有字时 Label 浮动到上面)。

  • 原理: 当输入框显示 placeholder 时(意味着里面没字),应用一种样式;不显示 placeholder 时(意味着用户输入了内容),应用另一种样式。
css
/* 当输入框里什么都没写(显示占位符)时 */
input:placeholder-shown {
  border-color: #ccc;
}

/* 当输入框里有内容(占位符消失)时 */
input:not(:placeholder-shown) {
  border-color: green; /* 只有输入了内容才变绿 */
}

@starting-style & allow-discrete

这是前端开发者等了 10 年的功能。它解决了**“如何对 display: none 进行动画过渡”**这个千古难题。

痛点: 当你把一个元素从 display: none 变成 display: block 时,浏览器会瞬间显示它,CSS 的 transition: opacity 1s 会失效。因为浏览器认为它之前“不存在”,无法计算过渡。 以前的 Hack: 使用 setTimeout 延迟添加类名,或者强制触发回流(Reflow),非常丑陋。 现代解法: 告诉浏览器这个元素“刚出来时”长什么样。

代码示例(完美的淡入淡出):

css
.modal {
  /* 1. 必须开启这个,允许 display 属性参与离散动画 */
  transition-behavior: allow-discrete;
  /* 常规过渡 */
  transition: opacity 0.5s, display 0.5s;
  /* 或者这么定义 */
  /* 核心:开启离散属性过渡行为 */
  transition: 
    display 0.5s allow-discrete, 
    overlay 0.5s allow-discrete, 
    opacity 0.5s, 
    transform 0.5s;
  /* 默认状态(隐藏时) */
  display: none;
  opacity: 0;
}

/* 2. 打开状态 */
.modal.open {
  display: block;
  opacity: 1;
  /* 3. 核心:定义元素"刚被插入 DOM 这一帧"的样式 */
  @starting-style {
    opacity: 0; /* 告诉浏览器:它虽然变成 block 了,但起始透明度是 0 */
  }
}

image-20260213180059189

网格的救星:subgrid

subgrid 出现之前,CSS 的网格就像是一道围墙:一旦你进入了子元素内部,外面的网格坐标系就失效了。为了让不同卡片里的按钮水平对齐,我们以前不得不给标题设死高度(极其不响应式)或者动用大量的 JS 动态计算。

为什么它是“救星”?

  1. 共享生命周期: 在普通的嵌套网格中,子元素的尺寸只由其内部内容决定。而 subgrid 让子元素参与到父网格的 “尺寸协商” 中。如果其中一个子元素的标题变高了,它会撑开父网格的那一行,从而同步拉开所有同级卡片的标题行。
  2. 继承间距 (gap): 默认情况下,subgrid 也会继承父容器的 gap。这保证了整张页面视觉节奏的绝对统一。
  3. 支持多维度: 你可以只对行使用 subgridgrid-template-rows: subgrid),也可以只对列使用,或者两者都用。

代码示例:

css
.card-container {
  display: grid;
  /* 定义三行:标题、内容、页脚 */
  grid-template-rows: auto 1fr auto; 
}

.card {
  /* 让卡片内部也变成网格,并且直接复用父容器定义的行高 */
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid; /* 魔法在这里 */
}

image-20260213175416235

锚点定位

这是 CSS 目前最复杂但也最强大的布局系统。它专门用来解决“浮动元素定位”的问题(比如 Tooltip 必须跟着按钮走)。

痛点: 以前为了让一个提示框(Tooltip)精准地对齐一个按钮,我们需要引入 Popper.jsFloating UI 这样沉重的 JS 库来计算坐标。

现代解法: CSS 原生支持“把 A 元素钉在 B 元素旁边”,哪怕它们在 DOM 结构里相隔十万八千里。

代码示例:

css
/* 1. 给按钮起个名字 */
.my-button {
  anchor-name: --btn-trigger;
}

/* 2. 让提示框去找这个名字 */
.my-tooltip {
  position: absolute / fixed;
  /* 核心:我要定位到 --btn-trigger 这个锚点 */
  position-anchor: --btn-trigger;
  /* 魔法指令:我的顶部,对齐锚点的底部 */
  top: anchor(bottom);
  /* 我的左边,对齐锚点的左边 */
  left: anchor(left);
}

为什么牛? 如果页面滚动,按钮跑了,提示框会自动跟着跑,不需要 JS 实时重算坐标!

image-20260213174734401

位于同一层级的建立关系

现代化输入框历史

如何让输入框(尤其是文本域)随着内容的增加自动变高/变宽,而不需要写一行 JavaScript 代码。

传统输入框

当输入文字之后会溢出(水平滚动),只能显示单行image-20260213171730502

接下来展示一下textarea

image-20260213172058497

可以展示多行,但是不会自动变宽变高

对于contenteditablediv来说,是会自动增高的:image-20260213172556622也会自动变宽:image-20260213173107031

field-sizing: content

这是目前最火的输入框“自适应”属性。

以前的做法: 如果想让 <textarea> 自动随文字增多而长高,你需要监听 input 事件,然后用 JS 动态计算 scrollHeight 并赋值给 height,非常麻烦。

现在的做法: 一行 CSS 搞定。

css
textarea, input {
  /* 核心属性:让尺寸根据内容自动调整 */
  field-sizing: content;
  /* 建议搭配 min-width 或 min-height 防止初始太小 */
  min-width: 100px; 
}

效果:

当你在输入框打字时,宽度会随着文字自动变宽(如果是 input)。

image-20260213173327615

当你在文本域换行时,高度会自动变高(如果是 textarea)。

image-20260213173510904

注意: 这是一个比较新的属性(Chrome 123+ 支持),兼容性正在快速普及中。

field-sizing 出现之前,contenteditable 确实是实现“高度自适应输入框”最流行、视觉效果最流畅的“黑科技”方案之一。

但它其实是一个“带刺的玫瑰”,虽然解决了高度自适应的问题,却带来了很多其他麻烦。

下面我们来复盘一下以前的几种实现方案,看看我们为了“让输入框自动变高”到底经历了什么:

contenteditable

这是利用 HTML 的原生特性:<div> 默认就是根据内容撑开高度的。只要给它加上 contenteditable="true",用户就能在里面打字,高度自然就随之变化了。

css
<div contenteditable="true" class="input-box"></div>

<style>
.input-box {
  border: 1px solid #ccc;
  min-height: 40px;
  /* 必须处理,否则没有 input 那样的 padding 和 outline */
  padding: 10px; 
}
</style>

❌ 这种方案的致命缺点(这也是为什么我们急需 field-sizing):

  1. 不是表单元素: 它是一个 div,不能直接用于 <form> 提交。你必须写 JS 把它的 innerText 复制到一个隐藏的 <input> 里才能提交。
  2. 富文本污染: 用户如果从网页上复制一段带格式的文字(粗体、大号字、红色)粘贴进去,div 会照单全收,把它渲染成 HTML。这对于只需要纯文本的输入框(比如评论区)来说是灾难。
    • 补救措施: 后来多了一个属性 contenteditable="plain-text-only",但兼容性一直不好。
  3. 字数限制失效: <input>maxlength 属性对它无效,你得自己写 JS 监听字数。
  4. 无障碍性差: 屏幕阅读器对它的支持不如原生 textarea

Textarea + JS 暴力计算

这是最稳妥、最传统的商业项目做法。使用原生的 <textarea>,但是监听用户的输入,每打一个字就算一次高度。

js
// 以前的经典 JS 写法
const textarea = document.querySelector('textarea');
textarea.addEventListener('input', function() {
  // 1. 先重置高度,防止删除文字后缩不回去
  this.style.height = 'auto'; 
  // 2. 设置为 scrollHeight (内容真实高度)
  this.style.height = (this.scrollHeight) + 'px'; 
});

❌ 缺点:

  • 性能消耗: 每次输入都触发重排(Reflow),在低端机上打字快了会有卡顿感。
  • 闪烁: 因为要先设为 auto 再设为具体像素,有时候会看到输入框瞬间“抖动”一下。
  • 需要写 JS: 这种纯样式的需求,本不该依赖 JS。

CSS Grid “复制人” 战术

field-sizing 出来的前夕,这被认为是最优雅的 CSS-only 方案

原理:

  1. 搞一个 Grid 容器。
  2. 里面放一个 <span> 和一个 <textarea>
  3. 关键点:<textarea><span> 重叠在同一个格子里。
  4. <span> 设为 visibility: hidden,并把 <textarea> 输入的内容实时同步给 span(还是需要一点点 JS,或者 React/Vue 双向绑定)。
  5. 结果: span 撑开了父容器的高度,textarea 设为 width: 100%; height: 100%,于是 textarea 就被撑大了。
css
/* 这种代码写起来非常掉头发 */
.container {
  display: grid;
}
.container::after {
  /* 这是一个伪元素,用来模拟内容 */
  content: attr(data-value) " ";
  visibility: hidden;
  white-space: pre-wrap; 
  grid-area: 1 / 1 / 2 / 2; /* 放在第一格 */
}
textarea {
  grid-area: 1 / 1 / 2 / 2; /* 也放在第一格 */
  resize: none;
  overflow: hidden;
}

总结

所以,你提到的 contenteditable 确实是以前很常见的一种模拟手段

而现在的 field-sizing: content 之所以被称为“史诗级更新”,就是因为它:

  1. 是原生的表单元素(支持 name, value, required, maxlength)。
  2. 没有富文本污染(只能输入纯文本)。
  3. 性能极佳(浏览器层面的渲染优化)。
  4. 0 行 JavaScript

:target伪类选择器

:target 代表 当前 URL 中哈希符号(#)指向的 HTML 元素

简单来说,当用户点击了一个指向页面内某个 ID 的链接时,那个 ID 对应的元素就是 :target

核心用法

它常用于制作无需 JS 的:弹出层、手风琴折叠面板、或者高亮当前阅读的章节

html
<a href="#section1">跳转到第一部分</a>

<section id="section1">
  <h2>第一部分</h2>
  <p>内容...</p>
</section>

<style>
  /* 当这个 section 成为目标时,改变背景色并增加缩放效果 */
  section:target {
    background: #fff9c4;
    border: 2px solid #fbc02d;
    transform: scale(1.02);
    transition: all 0.3s;
  }
</style>

小技巧:你可以利用它做一个简单的模态框(Modal)。点击按钮跳转到 #modal 显示,点击模态框内的关闭按钮(链接到 #)即可隐藏。

z-index内核

https://www.joshwcomeau.com/css/stacking-contexts/

在 CSS 中,我们可以使用一个工具来显式控制 HTML 元素的堆叠顺序:z-index。值较高的元素将显示在顶部。

image-20260214194933821

因为.first.box的 z-index 值大于 .second.box,所以它会排在前面。如果我们移除 z-index 声明,它就会排在后面。

然而,事情并非总是如此简单。有时,较大的 z-index 值并不一定能带来更好的效果。来看看这里发生了什么:

image-20260214195226933

.tooltip它的z-index 值比header大得多!,那么为什么标题会显示在最上面呢?

为什么不是这样: image-20260214195520069

要解开这个谜团,我们需要了解堆叠上下文(stacking contexts),这是一种晦涩但至关重要的 CSS 机制。在本文中,我们将探讨堆叠上下文是什么、它是如何工作的,以及如何利用它来发挥我们的优势。

在 CSS 中,工作原理类似PS:元素被分组到堆叠上下文中。当我们给一个元素设置 z-index 值时,该值只会与同一上下文中的其他元素进行比较。z-index 值并非全局的。

默认情况下,纯 HTML 文档只有一个包含所有节点的堆叠上下文。但我们可以创建额外的上下文!

创建堆叠上下文的方法有很多种,但以下是最常见的一种:

css
.some-element {  position: relative;  z-index: 1;}

通过结合这两个声明,可以触发一个秘密机制:创建一个堆叠上下文,围绕该元素及其所有子元素形成一个组。

让我们从上方再审视一下这个问题:

html
<style>
  header {
    position: relative;
    z-index: 2;
  }
  .tooltip {
    position: absolute;
    z-index: 999999;
  }
  main {
    position: relative;
    z-index: 1;
  }
</style>

<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>

我们可以绘制出这段代码中创建的堆叠上下文:

image-20260214195836898我们的 .tooltip 元素虽然设置了 999999 这种“天花板级别”的 z-index,但这个数字只在 <main> 这个小圈子(堆叠上下文)里才管用。它只能决定tooltip是压在旁边的 <p> 标签上面还是被压在下面,仅此而已,它越不了狱。

与此同时,在父级上下文中,浏览器会对比 <header><main>。由于 <main>z-index 值较小,它会显示在 <header> 下方。而它所有的子元素也会随之被压在下面。

这就好比语义化版本号(Semantic Versioning)。

我知道并不是每个人都有使用 Photoshop、Figma 或 Sketch 等软件的经验。如果上面的类比没能让你产生共鸣,我有另一个你更有可能熟悉的类比:语义化版本号。

在语义化版本号中,不同“级别”的版本由点分隔。例如,一个包的 2.0 版本比 1.0 版本高,同时也比 1.999 版本高。

z-index 就像版本号,而堆叠上下文就像版本级别。每当创建一个堆叠上下文,我们就在版本号中添加一个点:

html
<header> <!-- 2.0 -->
  My Cool Site
</header>
<main> <!-- 1.0 -->
  <div class="tooltip"> <!-- 1.999999 -->
    A tooltip
  </div>
</main>

我们的工具提示会显示在 <header> 下方,因为 1.999999 是比 2.0 更低的版本。无论我们在次要版本号加多少个 9,它永远无法超越更大的主版本号。

修复我们的示例

我们该如何解决工具提示的问题呢?在这个例子中,我们实际上并不需要为 <main> 创建堆叠上下文:

image-20260214200600231 在没有 z-index 的情况下,<main> 将不会创建堆叠上下文。那么,我们的层级结构如下所示:

image-20260214200709654

因为 header 和我们的工具提示现在处于同一个上下文中,它们的 z-index 值将直接进行对决,最终工具提示胜出。

这里有一个重要的区别:我们讨论的并不是父子关系。即便tooltipheader 嵌套得更深也无所谓。浏览器只关心堆叠上下文。

打破规则

在这个刻意设计的示例中,我们可以移除 <main>z-index,因为它确实没起什么作用。但如果我们真的需要 <main> 使用 z-index 或创建堆叠上下文呢?

根据 CSS 规则,我们无法“摆脱”堆叠上下文的限制。处于某个堆叠上下文内部的元素永远无法与另一个上下文中的元素进行层叠比较。

然而,通过一些“打破常规”的思考,我们依然可以实现理想的效果。

我们可以将tooltip附加到 <body> 标签上,从而使其在 <main> 之外进行渲染。然后,我们可以利用 CSS 对其进行定位,使其在视觉上看起来像是该元素的子元素。

创建堆叠上下文

我们已经看到如何通过结合相对定位(relative)或绝对定位(absolute)与 z-index 来创建堆叠上下文,但这并不是唯一的方法!以下是其他一些方式:

  • opacity(透明度)设置为小于 1 的值。
  • position 设置为 fixed(固定定位)或 sticky(粘性定位)(这些值不需要 z-index 即可生效!)。
  • 应用 normal(正常)以外的 mix-blend-mode(混合模式)。
  • display: flexdisplay: grid 容器的子元素上添加 z-index
  • 使用 transform(变换)、filter(滤镜)、clip-path(裁剪路径)或 perspective(透视)。
  • 使用 will-change 属性并设置如 opacitytransform 之类的值。
  • 使用 isolation: isolate 显式创建上下文(稍后会有详细介绍!)。

There are a few other ways as well. You can find the full list on MDN(opens in new tab).

关于 z-index 的一个常见误解

为了让 z-index 生效,我们需要将 position 设置为 relativeabsolute 之类的值,对吧?

并不完全如此。看看这里发生了什么:

image-20260214201254863 第二个盒子通过使用 z-index 提升到了其兄弟元素之上。然而,在这段代码片段中没有任何地方声明了 position 属性!

我曾经认为 z-index 属性对 position 属性有硬性依赖,即必须将 position 设置为 relativeabsolute 之类的值才能使用它。但事实并非完全如此。

从核心层面来看,CSS 是布局算法的集合。这其中包括 Flexbox、Grid、流式布局(Flow layout,默认布局)以及许多其他布局。每种布局算法都可以按照自己的意愿实现每一个 CSS 属性。

当我们设置 position: relativeposition: absolute 时,我们切换到了定位布局(Positioned Layout),这是一种实现了 z-index 的布局算法。但它并不是唯一实现该属性的算法!Flexbox 和 Grid 也都实现了它。

等一下……

这里有个古怪的地方,我觉得值得花上一两分钟思考一下。

在我们的 Photoshop 类比中,组(Group)和图层(Layer)之间有着明确的区别。所有的视觉元素都是图层,而组则是作为容纳它们的结构化辅助手段而存在的。它们是截然不同的概念。

然而,在 Web 上,这种区别就不那么清晰了。每一个使用了 z-index 的元素,同时也必须创建一个堆叠上下文。

当我们决定给一个元素设置 z-index 时,我们的目标通常是让该元素在父级堆叠上下文中相对于其他元素提升或降低层级。我们并不是有意要在这个元素上产生一个堆叠上下文! 但考虑到这一点至关重要。

当一个堆叠上下文被创建时,它会“压平”其所有的后代元素。这些子元素在内部依然可以重新排列,但本质上,我们已经把这些子元素“锁死”在里面了。

让我们再看一眼之前的标记代码:

html
<header>
  My Cool Site
</header>
<main>
  <div class="tooltip">
    A tooltip
  </div>
  <p>Some main content</p>
</main>

默认情况下,HTML 元素会根据它们的 DOM 顺序进行堆叠。在没有任何 CSS 干预的情况下,<main> 将渲染在 <header> 之上。

我们可以通过给 <header> 设置 z-index 来将其提升到最前面,但这必然会压平它所有的子元素。正是这种机制导致了我们之前讨论的那个 Bug。

我们不应该仅仅将 z-index 视为改变元素顺序的方法。我们应该将其视为在元素子项周围形成一个“组”的方法。除非形成一个组,否则 z-index 就无法生效。

信不信由你,这反而是件好事

正如我们在工具提示(tooltip)演示中所看到的,堆叠上下文可能会导致一些隐蔽且难以诊断的 Bug。那么,如果所有的 z-index 值都在全局范围内进行比较,不是更好吗?

我不这么认为,原因有以下几点:

  • 现状下,z-index 的通货膨胀(即 z-index 值不断攀升的趋势)已经像一种流行病。 试想一下,如果网页上的每一个设置了 z-index 的元素都必须在同一个尺度下竞争,情况会变得多么糟糕?
  • 我不是浏览器工程师,但我猜想堆叠上下文有利于性能优化。如果没有它们,浏览器将不得不把每一个带有 z-index 的项与所有其他带有 z-index 的项进行比对。这听起来工作量要大得多。
  • 一旦我们理解了堆叠上下文,我们就可以利用它们来**“封锁(seal off)”**元素,将其转化为我们的优势。在像 React 这样以组件驱动的框架中,这是一种极其强大的模式。

最后一点尤其有意思,让我们深入探讨一下。

用“isolation”构建不透水的抽象

我最喜欢的 CSS 属性之一也是最冷门的属性之一。我想向你介绍 isolation 属性,它是这门语言中一颗隐藏的明珠。

它的用法如下:

cs
.wrapper {
  isolation: isolate;
}

当我们为一个元素应用此声明时,它只做一件事:创建一个新的堆叠上下文。

既然有这么多创建堆叠上下文的方法,为什么我们还需要另一种呢?因为在其他所有方法中,堆叠上下文都是作为某种其他变更的结果而隐式创建的。而 isolation 以最纯粹的方式创建堆叠上下文:

  • 无需预设 z-index 值。
  • 可用于静态定位(static)的元素。
  • 不会以任何方式影响子元素的渲染。

极其有用,因为它允许我们“封锁”一个元素的子项。

让我们来看一个例子。最近我构建了这个精致的信封组件。悬停或聚焦即可看到它打开:

image-20260214202351619

它包含了好几层(从左到右:FlapShellLetterBack Panel):

image-20260214202422118

代码类似:

<div>
  <BackPane style={{ zIndex: 1 }} />
  <Letter style={{ zIndex: 3 }}>
    {children}
  </Letter>
  <Shell style={{ zIndex: 4 }} />
  <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
</div>

(如果你在好奇为什么 Flap(信封翻盖)拥有动态的 z-index,那是因为当信封打开时,它需要移动到信件的后方。)

一个优秀的 React 组件应该像航天服一样,与其周围环境完全隔离开来。然而,这件航天服却漏气了。看看当我把它放在一个 z-index: 3<header> 附近时会发生什么:

我们的 <Envelope> 组件将这 4 个层级包装在一个 div 中,但它并没有创建一个堆叠上下文。结果,这些层级可能会与其他组件“缠绕”在一起,就像是在玩一场世界上最无聊的《扭扭乐》游戏。

通过在 <Envelope> 的顶层元素上使用 isolation: isolate,我们就能保证它作为一个整体进行定位:

function Envelope({ children }) {
  return (
    <div style={{ isolation: 'isolate' }}>
      <BackPane style={{ zIndex: 1 }} />
      <Letter style={{ zIndex: 3 }}>
        {children}
      </Letter>
      <Shell style={{ zIndex: 4 }} />
      <Flap style={{ zIndex: isOpen ? 2 : 5 }} />
    </div>
  )
}

为什么不使用老办法,通过 position: relative; z-index: 1; 来创建堆叠上下文呢?这是因为 React 组件应该是可复用的;在所有情况下,1 真的都是这个组件合适的 z-index 值吗?isolation 的妙处在于它能让我们的组件保持中立和灵活。

浏览器支持

isolation 属性并非新事物,它拥有非常好的浏览器支持:除了 Internet Explorer 之外,它在所有浏览器中都能正常工作。

如果我需要支持 Internet Explorer,我会考虑改用 transform: translate(0px);。虽然我还没有测试过,但我相信它能达到同样的效果:在不产生任何实质性副作用的情况下创建一个堆叠上下文。

调试堆叠上下文问题

如果你曾为了让元素按正确顺序堆叠而苦苦挣扎,你就会明白调试堆叠上下文问题是多么棘手。幸运的是,我有一些可能会有所帮助的工具!

Microsoft Edge 有一个很有意思的“3D 视图”,允许我们查看堆叠上下文:

深入探索

堆叠上下文是 CSS 构建在“隐藏机制”之上的一个典型例子。你可能花了数年时间用 CSS 构建界面,却从未意识到它们的存在。

除非你专门花时间去学习这些机制,否则你的思维模型(Mental Model)将永远缺失一部分。而只要你的思维模型有哪怕一点点偏差,这种差异迟早会导致问题的产生。

CSS 没有警告,也没有错误提示。当一些出人意料的情况发生时,没有明确的“下一步”来告诉你哪里出了错。这些挫折会打断我们的“心流”状态,动摇我们的信心。我认为这就是为什么如此多的前端开发者不喜欢写 CSS 的原因。

然而,一旦你对这门语言建立起直觉,写 CSS 就会变成一种纯粹的享受。现在的我非常热爱写 CSS。

我想帮助其他开发者也发现这种乐趣。为此,我创建了一门全面的自学在线课程,旨在从更深层次解释 CSS 的运行原理,并教授我每天用来构建各种用户界面的实用技巧。

这门课叫作**“CSS for JavaScript Developers”**,现在已经上线了。 😄

堆叠上下文的终结者:isolation: isolate

https://dev.to/francescovetere/the-css-property-you-didnt-know-you-needed-3fk0

这是一个只有资深前端才懂的“救命属性”。

痛点: 你用 z-index 试图把一个弹窗置顶,结果发现不管设成 9999 还是 999999,它死活被压在一个 z-index: 1 的元素下面。

原因: 你的弹窗被困在一个**“局部堆叠上下文 (Stacking Context)”**里了。它在那个小圈子里是老大,但出了圈子,整个圈子都被别人压在下面。 以前的 Hack: 给父元素加 transform: translateZ(0) 或者 opacity: 0.99 来强制创建一个新的堆叠上下文。这属于“副作用编程”,很难维护。

现代解法: 显式地创建一个新的上下文。

css
.card {
  /* 核心:告诉浏览器,我是一个独立的堆叠环境 */
  /* 我内部的 z-index 再乱,也不会影响外部;外部也别想随便插进我内部来 */
  isolation: isolate; 
  /* 典型场景:配合 mix-blend-mode 使用 */
  /* 如果不加 isolate,文字反色可能会反到 body 背景上去,而不是卡片背景 */
}

底蕴价值: 这代表了你对 浏览器渲染层级 (Layer Model) 的深刻理解。它能让你的组件样式极其健壮,不会因为被搬到页面的另一个角落就突然乱了层级。

现在,假设我们想在卡片的右上角添加一个引号图片,作为一点****装饰元素。就像这样:

image-20260214192000335

我们可以把引号图片当作伪元素,然后把position: absolute它放在卡片上方!

我们得到的结果如下:

image-20260214192043900

看起来不错,只有一个小问题:我们想把这张图片放在文字后面

这就是我们产品z-index发挥作用的地方。一点点z-index: -1就够了吧?嗯……也不尽然😕

image-20260214192059790

😱 它……消失了?!

按照浏览器渲染规则,定位元素(Positioned) 的层级默认高于 非定位元素(Non-positioned)

结果: 那个巨大的紫色引号会直接覆盖在你的标题和正文上面。

如果按照下面这种设置就没问题:

css
.card{
  position: relative;
  z-index:2;
  ::after{
    content: "“";
    position:absolute;
    right: 1rem;
    top: 0;
    font-size: 10rem;
    color: #a675ee;
    z-index: -1;
  }

这是为什么?

是因为你触发了 CSS 中一个隐藏的**“保底逻辑”**:任何 z-index 不为 auto 的定位元素,都会自动创建一个新的堆叠上下文(Stacking Context)。

为什么你的 .card 没有创建堆叠上下文?

根据 CSS 规范,对于定位元素relative, absolute),要创建堆叠上下文,必须满足:

position 的值不是 static,并且 z-index 的值不是 auto

仔细想想,这完全说得通:默认情况下,每个元素都会获得一个图层。所以,这就好比说,默认情况下,z-index: 0页面上的每个元素都位于同一个图层上。

z-index: -1当我们在伪元素上声明一个属性时,实际上是将其推到了默认层之下

🤔 我们该如何解决这个问题?

理想情况下,我们希望有一个局部作用域:当我说我希望我的伪元素有一个时**,** 我真正的意思是:“位于所有内容之后,但不要位于卡片本身之后!”z-index: -1

✅ 这个概念实际上已经内置在 CSS 中,它被称为上下文堆叠!

堆叠上下文本质上是由一个元素定义的,该元素充当其子元素声明的根元素z-index。特别地,无论子元素的z-index位置有多低,它们都永远不会脱离其堆叠上下文。

默认情况下,纯HTML 文档只有一个堆叠上下文,每个元素都引用它。这就是为什么当我们给z-index: -1伪元素赋值时,它会被推到最底层:因为只有一个全局堆叠上下文!

💡当然,我们也可以在任意元素上创建新的堆叠上下文!

我们可以通过多种方式为元素创建新的堆叠上下文。以下列举几种方法:

  • 给它分配一个transform
  • 给它分配一个filter
  • 给它分配一个opacity

您可以在这里找到所有房产列表。

所有这些方法都有一个很大的问题:它们都会产生副作用! 😢

isolation这是一个特殊属性,其唯一且唯一的作用是在应用它的元素上**创建一个新的堆叠上下文,**没有任何其他副作用。😄

它接受两个值,autoisolate。当然,isolate是我们想要的。

就是这样!这个问题的最终解决方案其实非常简单:

css
.card {
  position: relative;
  isolation: isolate;
}

穿透影子 DOM 的利剑:::part()

这是 Web Components 和现代组件库(如 Element Plus, Shoelace)的核心。

你引入了一个第三方的 <my-player> 视频播放器组件。 这个组件内部是封闭的 Shadow DOM(影子 DOM)。 你想把播放按钮变成红色,但你的全局 CSS my-player .play-btn { color: red } 完全无效,因为样式进不去。

现代解法: 组件开发者在内部“开一个口子”,你从外部注入样式。

css
/* 组件内部代码 (你看不见,但开发者写了): */
/* <button part="play-button">Play</button> */
/* 你的外部代码: */
my-player::part(play-button) {
    /* 核心:你可以直接修改这个“暴露出来”的部分 */
    background-color: red;
    border-radius: 50%;
}

底蕴价值: 这是 封装与开放的平衡。它比 ::v-deep (Vue) 或 :global (React Modules) 更标准、更安全。它像是一个 API 接口,只允许你修改开发者允许你修改的部分。

0-auto动画

grid(0fr-1fr)

html
<style>
  .container {
    display: grid;
    padding: 5px;
    background-color: aquamarine;
    grid-template-rows: 0fr;
    grid-gap: 20px;
    transition: grid-template-rows 0.5s;
    &:hover{
      grid-template-rows: 1fr;
    }
  }
  .content{
    overflow: hidden;
  }
</style>
<div class="container">
  <div class="content">
    <p>
      长内容
    </p>
  </div>
</div>

因为这个数值是0-1,所以都是数字,就可以使用到了transition

interpolate-size

interpolate-size (让 height: auto 动起来)

这是 2024 年下半年刚刚落地的超级新特性

你想做一个“手风琴”展开效果。默认状态 height: 0,展开状态 height: auto

问题: CSS 无法0auto 之间做 transition 动画。因为它不知道 auto 到底是多少像素。

以前的 Hack: 用 JS 算高度,或者用 max-height: 1000px(会导致动画时间不准)。

现代解法: 允许关键字插值。

html
<style>
  :root {
    /* 全局开启:允许浏览器计算 auto 的中间值 */
    interpolate-size: allow-keywords;
  }
  .content {
    overflow: hidden;
    height: 0px;
    transition: height .5s;
    padding: 10px;
    background-color: aquamarine;
    &:hover {
      height: auto;
    }
  }
</style>
<div class="content">
  <p>
  长内容
  </p>
</div>

使用 Flexbox

对于这种方法,将要过渡的元素包裹在两个 div 中:外层 div 是 flexbox,内层 div 是 flex item。这样,就可以实现max-height0100%的过渡。这利用了 flexbox 内部 height/max-height 属性的一个特性。

css
<style>
  .wrapper {
    display: flex;
    padding: 10px;
    background-color: palegreen;
    &:hover{
      .inner {
        max-height: 100%;
      }
    }
  }
  .inner {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.5s ease-out;
  }
</style>

wrapper请确保在元素和标签之间有一个额外的 divinner元素:

html
<div class="wrapper">
  <div>
    <div class="inner">
      长内容
    </div>
  </div>
</div>

这种方法的缺点在于,虽然内部元素可以平滑地进行动画,但它的容器却不会;容器会瞬间卡入到位。不过在某些情况下,这可能反而更好,因为浏览器不需要不断地重新排版其下方页面的所有内容。

这个方法核心利用了 Flexbox 布局模型中“伸缩项目(Flex Items)”的高度计算机制,特别是关于 百分比高度(Percentage Height)的解析规则

通常情况下,如果你对一个普通块级元素设置 max-height: 100%,由于其父元素的高度往往是由内容撑开的(即 height: auto),浏览器无法计算出具体的百分比值,导致 transition 动画失效。

但在你提供的这个 Flexbox 结构中,发生了以下有趣的事情:

1. Flex 项目的“自动拉伸”特性

.wrapper(Flex 容器)中,默认的 align-items 属性值是 stretch。这意味着它的直接子元素(即那个额外的 div)会被自动拉伸,以填满父容器的高度。

2. 建立了明确的高度基准

当外层 div 变成一个 Flex 项目并被“拉伸”时,它获得了一个确定的计算高度(由 .wrapper 的高度决定)。

  • 普通情况: 父元素高度是 auto,子元素 max-height: 100% = 无法计算,动画失败。
  • Flex 情况: 外层 div 被 Flex 引擎强制赋予了高度。此时,最内层的 .inner 看到的父元素不再是“不确定的”,而是一个有明确尺寸的参考框

3. 百分比的可计算性

由于父级(那个额外的 div)现在有了确定的高度基准,.innermax-height: 100% 就能解析为一个具体的像素值。只要目标值是可计算的像素值,CSS transition 就能在 0 和这个 100%(即具体的像素值)之间进行平滑插值。

这就是这个技巧最“玄学”也最聪明的地方:它确实没有硬编码的固定高度(如 200px),但它创造了一个“明确的相对高度”。在 CSS 规范中,百分比高度能否生效,取决于父元素是否具有 Defined Height(已定义高度)

其他方式为什么不行

html
<style>
.container{
  display: flex;
  flex-direction: column;
  padding: 10px;
  background-color: aquamarine;
  &:hover{
    .content{
      flex-basis: 200px;
    }
  }
  }
  .content{
    background-color: bisque;
    overflow: hidden;
    flex-basis: 0;
    transition: all .5s;
  }
</style>
<div class="container">
  <div class="content">
    <p>
      简短的回答是:AS174(Cogent Communications)本身并不是“原生住宅IP”提供商,它是一家全球领先的商业骨干网(Tier 1)运营商。
      如果你发现某个 IP 的 ASN 是 AS174,它在绝大多数情况下会被识别为 Data Center(数据中心/机房) 属性,而不是 Residential(住宅)。
      以下是详细的分析,帮助你理解为什么会有这种误解,以及如何判断
    </p>
  </div>
</div>

为什么会遇到那种困境?

flex-basis: 200px;如果换为:flex-basis: auto;就不可避免地遇到0-auto难题,必须要开启interpolate-size: allow-keywords;

评论

评论加载中……