position: sticky 只有短短一行,却长期被误用、误解、误判。很多人第一次写它的时候都会经历同样的三段式:兴冲冲加上 top: 0——毫无反应;上网一查,发现“父元素不能有 overflow: hidden”;好不容易改对了,又发现滚动到某个位置它突然消失了。粘性定位不是“写上去就生效”的属性,而是一份需要满足四个条件的契约。本篇 40 分钟深度长文,从粘性约束矩形讲到滚动性能,从导航栏、目录、表格表头、分组吸顶讲到移动端底部操作栏,配合五个可交互的实时演示,帮你把 sticky 这件事一次吃透。
从 JS 监听滚动到一行 CSS
在 position: sticky 普及之前,实现“吸顶导航”是一件需要写 JavaScript 的活儿。最朴素的写法是这样的:
const nav = document.querySelector('.nav');
let navTop = nav.offsetTop;
window.addEventListener('scroll', () => {
if (window.scrollY > navTop) {
nav.classList.add('fixed');
} else {
nav.classList.remove('fixed');
}
});
/* 问题清单: */
/* ① scroll 事件高频触发,容易造成掉帧 */
/* ② 切换 fixed 会让元素脱离文档流,内容瞬间跳动 */
/* ③ 需要手动处理窗口 resize、阈值变化、多断点 */
/* ④ 阈值硬编码,样式与逻辑耦合 */
而使用 sticky,同样的事情只需要两行:
position: sticky;
top: 0;
}
它的行为介于 relative 与 fixed 之间:元素平时老老实实待在文档流里(像 relative),一旦滚动越过了你设定的阈值,就“粘”在视口的指定边缘(像 fixed),但永远无法超出它的父容器范围。这三点,构成了理解 sticky 的全部基础。
核心心智模型:粘性约束矩形
规范里有一个非常重要的概念,叫做 粘性约束矩形(sticky constraint rectangle)。你可以这样理解:
- 元素先按照正常文档流排布,得到一个“本来的位置”;
- 浏览器再检查这个位置是否越过了你设置的阈值;
- 如果越过了,就把元素向内“推”回阈值边界;
- 但推动的范围,不能超出元素的包含块(通常是父元素)。
第 4 条是关键。它决定了粘性元素的“活动范围”是父容器的高度,而不是整个页面。当父容器被滚出视口,粘性元素也会被一并带走——这就是为什么很多人会遇到“滚着滚着它就不见了”的现象。
.parent {
height: 200px; /* ← 活动范围上限 */
}
.child {
position: sticky;
top: 0;
height: 60px;
}
/* 结果:元素最多能粘住 140px 的距离 */
/* 200px(父高)- 60px(自身高)= 140px */
父容器越高,能粘住的滚动距离越长
元素越高,剩余可粘住的行程越短
这条规则解释了两个最常见的困惑:
- 为什么侧边栏会“滚到一半就不粘了”?——因为侧边栏的父容器(那一列的包裹元素)高度只有内容那么高,粘性元素一旦到达父容器底部就被带走了。
- 为什么分组标题会被下一个标题“顶走”?——因为每个标题的父容器就是它所属的分组,下一个分组开始时,前一个标题自然到达了它的活动范围边界。
生效的四个必要条件
当你写下 position: sticky 却发现没有效果时,99% 的情况是下面四条中有一条没满足。把它们背下来,可以省下大量调试时间。
top / right / bottom / left 至少有一个不为 auto
overflow: hidden/auto/scroll 的祖先(除非它就是滚动容器本身)
关于条件 2:为什么必须写 top?
这是一个极其容易踩的坑。很多人以为只要写 position: sticky 就会自动粘住顶部——不会。sticky 是“相对于某个阈值做粘性”,没有阈值,就没有粘性。这条规则的存在,让同一个元素可以粘在顶部、底部、左侧或右侧,完全由你决定。
.no-threshold {
position: sticky;
}
/* ✅ 粘在顶部 */
.stick-top { position: sticky; top: 0; }
/* ✅ 粘在底部 */
.stick-bottom { position: sticky; bottom: 0; }
/* ✅ 粘在左侧(横向滚动场景) */
.stick-left { position: sticky; left: 0; }
阈值是“触发线”,不是“偏移量”
这是另一个高频误解。top: 20px 的含义不是“让元素向下移动 20px”,而是:当元素的顶部边缘距离滚动视口顶部 20px 时,开始吸附。也就是说,粘住之后,元素会停留在距离顶部 20px 的位置。
.nav-a { position: sticky; top: 0; }
/* → 紧贴视口顶部 */
.nav-b { position: sticky; top: 16px; }
/* → 粘住后上方留出 16px 空隙 */
.nav-c { position: sticky; top: -1px; }
/* → 向上超出 1px,用于消除边框缝隙 */
为什么 top: -1px 是个好技巧?
当粘性表头紧贴容器顶部时,表头下方的边框和第一行内容之间,经常会出现一条 1px 的缝隙——这是子像素渲染与边框折叠共同作用的结果。把 top 设为 -1px,让元素向上多“吃”掉 1 像素,缝隙就消失了。这个技巧在表格、列表分组标题中非常常用。
交互实验室:亲手把 sticky 玩坏再修好
下面这个实验室把 position、top 阈值和祖先元素 overflow 三个变量都暴露出来。建议你按这个顺序操作,体验会很深刻:
- 先保持默认状态,滚动一下中间的盒子——元素会正常吸附,状态标签变成绿色的“已吸附”;
- 把
top改成“未设置”——吸附立刻失效,元素跟着内容一起滚走; - 把
top改回0px,再把祖先overflow改成hidden——明明样式没变,吸附却再次失效。
↓ 请在这个区域内上下滚动,观察上方导航条的行为 ↓
第一段:滚动容器内的普通内容。粘性元素在文档流中占据原本的位置,只是当它快要滚出可视区域时,被浏览器“按住”了。
第二段:粘性定位最大的特点是——它不会脱离文档流。相比 position: fixed,你不需要为它预留额外的占位元素,也不会在切换瞬间产生内容跳动。
第三段:注意观察导航条的颜色变化。当它处于“吸附”状态时,背景会从蓝色变成绿色,同时出现阴影。这是通过 JavaScript 读取 getBoundingClientRect() 实时计算出来的。
第四段:现在试着把上方工具栏里的“祖先元素 overflow”切换成 hidden。你会发现导航条不再粘住了——这是粘性定位最臭名昭著的陷阱。
第五段:原因在于 overflow: hidden 会创建一个新的滚动容器,粘性元素会以它为参照物来计算位置。可它本身并不会滚动,所以元素永远无法“吸附”到任何东西上。
第六段:把 overflow 改回 visible,一切恢复正常。记住这条规则:粘性元素的祖先链上,不能有会创建滚动容器的属性。
第七段:再来试试把 top 改成“未设置”。position: sticky 本身不带任何方向,必须配合 top、bottom、left、right 之一才能生效。
第八段:最后试试 top: 24px。粘住之后,元素会停在距离容器顶部 24px 的位置,而不是紧贴边缘——这就是“阈值”和“偏移”的区别。
第九段:继续滚动,让内容走完。你会发现元素始终稳稳地待在那个位置,直到它的父容器底部到达为止。
第十段:这就是“粘性约束矩形”的实际表现。父容器越高,能粘住的行程越长;父容器一旦滚出视口,粘性元素也会随之离开。
实战一:粘性导航栏与吸附反馈
粘性导航栏是最常见的应用场景,但真正做得好,还需要在“吸附瞬间”给出视觉反馈——比如加阴影、变半透明、加毛玻璃。这个反馈能让用户清楚地知道:现在导航栏已经浮在内容之上了。
.site-nav {
position: sticky;
top: 0;
z-index: 100; /* 必须显式设置 */
background: rgba(255,255,255,0.92);
backdrop-filter: blur(12px);
transition: box-shadow .3s ease;
}
.site-nav.is-stuck {
box-shadow: 0 4px 20px rgba(15,23,42,.12);
}
/* JS:哨兵元素 + IntersectionObserver(推荐) */
const nav = document.querySelector('.site-nav');
const sentinel = document.createElement('div');
nav.parentNode.insertBefore(sentinel, nav);
new IntersectionObserver(([entry]) => {
nav.classList.toggle('is-stuck', !entry.isIntersecting);
}).observe(sentinel);
吸附后添加 is-stuck 类,产生阴影与毛玻璃
为什么推荐用“哨兵元素 + IntersectionObserver”?
- 零滚动监听:不需要绑定
scroll事件,浏览器在合成线程完成判断,性能更好。 - 天然准确:哨兵元素的可见性变化,精确对应“导航栏是否已越过顶部”,不需要手动计算阈值。
- 代码量小:不到十行,且不依赖任何滚动位置数值。
当然,如果你只是想给元素加个阴影,也可以直接使用 CSS 的滚动驱动动画(后文会讲),完全不用 JavaScript。
实战二:粘性侧边目录
长文页面的侧边目录是粘性定位最经典的应用。它的实现非常简单,但有一个前提条件经常被忽略:侧边栏所在的列必须比目录本身高。
.layout {
display: grid;
grid-template-columns: 180px 1fr;
gap: 32px;
/* ✅ 默认 stretch:两侧列等高,侧栏有活动空间 */
}
/* ❌ 一旦改成 start,侧栏会被压成内容高度 */
.layout.broken {
align-items: start;
/* 侧栏高度 = 目录自身高度 → sticky 无处可粘 */
}
/* 目录本体 */
.toc {
position: sticky;
top: 24px; /* 给已悬浮的导航留出空间 */
max-height: calc(100vh - 48px);
overflow-y: auto; /* ⚠️ 注意:加在自身没问题 */
}
一、核心概念
粘性定位是相对定位与固定定位的混合体,元素在滚动过程中会在“正常流位置”和“吸附位置”之间切换。
二、约束矩形
元素的活动范围被限制在包含块内部,这是理解“为什么滚到一半就不粘了”的关键。
三、必要条件
滚动容器、阈值、活动空间、无 overflow 截断——四个条件缺一不可。
四、常见陷阱
overflow 截断、父容器高度不足、flex 的 align-items: center 导致高度塌陷、z-index 层叠错乱。
五、性能优化
粘性定位的布局开销很小,但要避免在粘性元素上做会引起重排的动画。
滚动上方区域,观察左侧目录如何吸附
进阶:让目录项跟随滚动高亮
光有粘性还不够,一个好的目录还应该高亮当前阅读位置。这可以用 IntersectionObserver 实现,关键是设置合适的 rootMargin:
const headings = document.querySelectorAll('.article-main h2[id]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const id = entry.target.id;
links.forEach(a => {
a.classList.toggle('active', a.hash === '#' + id);
});
});
}, {
/* 关键:把“触发带”压缩到视口上方 20% 处 */
rootMargin: '-20% 0px -70% 0px'
});
headings.forEach(h => observer.observe(h));
rootMargin: '-20% 0px -70% 0px' 的含义是:把观察区域的顶部向内收缩 20%,底部向内收缩 70%,只留下中间 10% 的一条“窄带”。当标题进入这条窄带时,就认为它“当前正在被阅读”。这个技巧在实践中非常好用。
实战三:粘性表格表头与首列冻结
数据表格是另一个粘性定位的高频战场。特别是当表格同时需要横向和纵向滚动时,表头粘顶 + 首列冻结就能极大提升可读性。
.table-scroll {
max-height: 400px;
overflow: auto;
}
/* ⚠️ 关键:必须用 separate,不能用 collapse */
/* border-collapse: collapse 会导致粘性表头边框丢失 */
.table {
border-collapse: separate;
border-spacing: 0;
}
/* 表头:粘顶 + 高层级 */
.table thead th {
position: sticky;
top: 0;
z-index: 3;
background: #0f172a; /* 必须有背景,否则内容透出 */
}
/* 首列:粘左 + 稍低层级 */
.table tbody td:first-child {
position: sticky;
left: 0;
z-index: 2;
background: white;
}
/* 交叉角落:最高层级,否则会被其他单元格盖住 */
.table thead th:first-child {
z-index: 4;
}
| 属性 | 作用轴 | 常用值 |
|---|---|---|
| position | — | sticky |
| top | 垂直 | 0 / 16px / -1px |
| bottom | 垂直 | 0 |
| left | 水平 | 0 |
| right | 水平 | 0 |
| z-index | 层叠 | 2 ~ 4 |
| background | 视觉 | 必须不透明 |
| border-collapse | 表格 | separate |
| overflow | 祖先 | 只能加在滚动容器上 |
| contain | 性能 | paint / layout |
| safe center | 对齐 | Chrome 115+ |
| scroll-margin | 锚点 | 配合粘性导航 |
同时向上下、左右滚动,观察表头与首列的冻结效果
为什么必须用 border-collapse: separate?
当表格使用 border-collapse: collapse 时,浏览器会把相邻单元格的边框合并渲染,此时 position: sticky 在单元格上会失效或表现异常。解决办法有两个:一是改用 separate 并手动设置 border-spacing: 0;二是改用 box-shadow 来模拟边框。前者更简洁,推荐优先使用。
实战四:通讯录式分组吸顶
手机通讯录、城市列表、消息列表——这类界面都有一个共同特征:分组标题会吸附在顶部,直到被下一个分组标题“顶走”。这恰恰是“粘性约束矩形”最直观的体现。
<div class="group">
<div class="group-head">A</div>
<div class="group-body">...</div>
</div>
/* 标题粘在所属分组内部 */
.group-head {
position: sticky;
top: 0;
z-index: 3;
background: #1e293b;
}
/* ⚠️ 千万不要给 .group 加 overflow: hidden */
/* 那会让标题失去可吸附的滚动上下文 */
.group {
/* overflow: hidden; ← 加了这个,吸顶立刻失效 */
}
滚动观察:每个分组标题会被下一个分组“顶走”
工作原理拆解:每个 .group-head 的父容器是它所属的 .g-group。当第一个分组滚出视口时,它的标题到达了父容器的底部边界,于是被“推”着向上离开;与此同时,第二个分组的标题刚好到达顶部,接管吸附状态。整个过程完全由浏览器计算,零 JavaScript。
实战五:底部吸附操作栏
sticky 不只能粘顶部。bottom: 0 可以让一个操作栏始终吸附在滚动容器的底部,直到滚到内容末尾才“归位”。这在移动端的表单页、详情页中非常实用。
.panel {
max-height: 70vh;
overflow-y: auto;
}
/* 底部操作栏 */
.panel-actions {
position: sticky;
bottom: 0;
z-index: 4;
background: white;
border-top: 1px solid #e2e8f0;
/* 关键:为 iPhone 底部安全区域留出空间 */
padding-bottom: env(safe-area-inset-bottom, 0);
}
/* 如果只是想让操作栏固定在视口底部 */
/* 那么用 position: fixed 反而更合适 */
.viewport-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
订单确认
商品:CSS 高级技巧实战手册 × 1
规格:电子版 · 终身更新
价格:¥ 99.00
优惠:新人立减 ¥ 20.00
实付:¥ 79.00
↓ 继续向下滚动,注意底部的操作栏 ↓
这个操作栏使用了 position: sticky; bottom: 0;。它的行为是:当元素本来的位置位于可视区域底部之下时,它会吸附在容器底部;当你滚动到内容末尾、元素回到正常位置时,它才“落回”文档流。
相比 position: fixed,粘性底部栏的最大优势是:它不会遮挡内容。因为它始终在滚动容器内部,滚动到末尾时会自动让出空间。
滚动到末尾,观察操作栏如何“落回”文档流
什么时候用 sticky,什么时候用 fixed?如果操作栏只在某个滚动区域内部生效(比如弹窗里的表单),用 sticky;如果它需要始终固定在浏览器视口底部(比如全局购物车栏),用 fixed。判断标准是:它属于哪个滚动上下文。
陷阱一:祖先元素的 overflow 截断
这是 sticky 失效的头号原因,没有之一。规则很简单,但很容易被忽视:
<div style="overflow: hidden"><div class="sticky">...</div></div>祖先元素上的
overflow 会创建一个新的滚动容器,粘性元素会以它为参照物。但它自己不滚动,于是元素永远无法吸附。
<div style="overflow: visible"><div class="sticky">...</div></div>让祖先保持
overflow: visible,或者把 overflow 加在真正的滚动容器上(也就是 sticky 元素的直接滚动父级)。
特殊情况:如果非要保留 overflow 怎么办?
实际项目中,我们经常需要在某个区域内部滚动,同时又希望内部的元素能粘住。这时候有两个选择:
/* 这是最常见、最推荐的做法 */
.scroll-panel {
overflow-y: auto; /* ← 滚动容器 */
max-height: 400px;
}
.scroll-panel .sticky-head {
position: sticky;
top: 0; /* ✅ 相对于 .scroll-panel 吸附 */
}
/* 方案 B:如果 overflow 只是为了清除浮动 */
/* 改用 display: flow-root,它不会创建滚动容器 */
.clearfix-safe {
display: flow-root; /* ✅ 现代替代方案 */
}
还有一个更隐蔽的情况:某些 UI 组件库(比如折叠面板、轮播组件)会在内部加 overflow: hidden 来裁剪内容。如果你的粘性元素恰好放在里面,就会莫名其妙失效。遇到这种情况,唯一的办法是调整 DOM 结构,把粘性元素移出那个容器。
陷阱二:父容器没有留下活动空间
粘性元素的活动范围被限制在父容器内。如果父容器的高度恰好等于元素本身,那么元素根本无处可“粘”。
.parent {
height: 60px;
}
.sticky-child {
position: sticky;
top: 0;
height: 60px;
}
/* → 元素完全没有移动余地 */
/* ✅ 让父容器比子元素高 */
.parent-ok {
min-height: 100vh; /* 或任意大于子元素的高度 */
}
/* ✅ 侧栏场景:让列与主内容等高 */
.layout {
display: grid;
grid-template-columns: 200px 1fr;
/* 默认 stretch 就是我们要的 */
}
陷阱三:flex 布局中的 stretch 与 center
在 flex 布局中,align-items 的取值会直接决定粘性元素有没有活动空间。这个坑在侧边栏布局中特别常见。
.layout {
display: flex;
align-items: center; /* 或 flex-start */
}
/* 此时 .sidebar 高度 = 内容高度 → 没有活动空间 */
/* ✅ 方案 A:使用默认的 stretch */
.layout-ok {
display: flex;
/* align-items: stretch 是默认值,不用写 */
}
.layout-ok .sidebar {
/* 现在侧栏会被拉伸到与主内容等高 */
}
/* ✅ 方案 B:手动给侧栏加高度 */
.layout-b .sidebar {
align-self: stretch;
/* 或者显式指定 min-height */
}
/* ✅ 方案 C:改用 grid,默认就是等高 */
.layout-c {
display: grid;
grid-template-columns: 200px 1fr;
/* 不需要任何额外设置,天然等高 */
}
position: sticky,但滚动时完全不粘,和普通元素没有区别。
align-items: center,改成默认的 stretch,或改用 grid 布局。
一个小技巧:在 DevTools 里选中侧栏元素,如果看到它的高度等于内容高度(而不是撑满整列),那基本就可以确定是这个原因了。
陷阱四:z-index 与层叠上下文
粘性元素在吸附时会覆盖在正常内容之上,如果没有设置 z-index,可能会被后面的元素盖住,或者反过来盖住不该盖的元素。
/* 遇到带 z-index 的兄弟元素时容易被遮盖 */
.sticky-a {
position: sticky;
top: 0;
/* z-index: auto → 可能被下方元素盖住 */
}
/* ✅ 显式声明层叠顺序 */
.sticky-b {
position: sticky;
top: 0;
z-index: 10;
}
/* 一个推荐的层叠顺序约定 */
/* 内容区 z-index: auto */
/* 粘性表头/首列 z-index: 2 ~ 4 */
/* 粘性分组标题 z-index: 5 */
/* 粘性导航栏 z-index: 100 */
/* 模态框遮罩 z-index: 1000 */
陷阱五:table 元素与 display 变更
表格元素的默认 display 是 table-row-group、table-row、table-cell,这些值对粘性定位的支持并不一致。
.table thead {
position: sticky;
top: 0; /* 各浏览器表现不一致 */
}
/* ✅ 推荐:加在 th / td 上 */
.table thead th {
position: sticky;
top: 0;
}
/* ⚠️ display: contents 会破坏粘性定位 */
/* 因为元素本身不再生成盒子,也就无从粘起 */
.broken {
display: contents; /* ← 慎用 */
}
/* ✅ 如果必须用 contents,把粘性加在子元素上 */
.broken > .sticky-child {
position: sticky;
top: 0;
}
性能考量:粘性定位贵不贵?
结论先说:粘性定位的性能开销非常小。它由浏览器的布局引擎原生实现,不需要 JavaScript 参与,也不会每帧触发脚本执行。但仍然有一些细节值得注意。
.scroll-panel {
contain: layout paint;
/* 告诉浏览器:内部变化不会影响外部 */
}
/* ✅ 用 content-visibility 跳过屏外渲染 */
.long-list .item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
/* ⚠️ 避免在粘性元素上做触发布局的动画 */
.sticky-bad {
position: sticky;
top: 0;
/* animation: height-change ...; ← 每次动画都会重排 */
}
/* ✅ 只动画 transform / opacity(合成层属性) */
.sticky-good {
position: sticky;
top: 0;
transition: box-shadow .3s ease, background .3s ease;
}
浏览器原生计算,无 JS 参与
取决于元素自身的绘制复杂度
不阻塞主线程
全现代浏览器支持
关于 will-change
有些人会在粘性元素上写 will-change: transform,希望提升滚动性能。这其实是个误区:sticky 的开销主要来自布局计算,而不是合成,will-change: transform 并不能解决问题,反而会强制创建合成层、增加内存占用。正确的做法是:如果粘性元素本身就是静态内容,什么都不用加;如果它的绘制很复杂,用 contain: paint 隔离即可。
当 sticky 不够用:滚动驱动动画
sticky 解决的是“吸附”问题,但有些需求比吸附更复杂。比如:随着滚动逐渐缩小的导航栏、随着滚动渐显的进度指示、随着滚动淡出的 Hero 区域。这类需求属于滚动驱动动画(Scroll-driven Animations)的范畴。
window.addEventListener('scroll', () => {
const p = Math.min(window.scrollY / 200, 1);
nav.style.height = (72 - 24 * p) + 'px';
});
/* 问题:每帧触发、强制同步布局、容易掉帧 */
/* 现代方案:纯 CSS 滚动驱动动画 */
@keyframes shrink {
to {
height: 48px;
padding: 4px 16px;
box-shadow: 0 4px 20px rgba(0,0,0,.12);
}
}
.nav {
position: sticky;
top: 0;
animation: shrink linear both;
animation-timeline: scroll(root);
animation-range: 0 200px;
}
/* 用 @supports 做渐进增强 */
@supports (animation-timeline: scroll()) {
/* 只有支持的浏览器才应用动画 */
}
选择建议:如果需求只是“粘住”,用 sticky;如果需求是“随滚动变化”,用滚动驱动动画。两者可以组合使用——比如一个粘性导航栏,同时在滚动过程中逐渐缩小。
兼容性与渐进增强
核心的 position: sticky 已经被所有现代浏览器支持多年,包括 IE11 的部分实现(IE11 支持 sticky,但对表格元素支持不佳)。真正需要注意的是较新的扩展语法。
/* position: sticky —— 全平台 */
/* top / bottom / left / right —— 全平台 */
/* z-index —— 全平台 */
/* 较新特性:需要渐进增强 */
/* animation-timeline —— Chrome 115+ / Safari 26+ */
/* scrollbar-width —— 部分支持 */
/* env(safe-area-inset-*)—— iOS Safari 11.2+ */
/* 用 @supports 做特性检测 */
@supports (position: sticky) {
.nav {
position: sticky;
top: 0;
}
}
/* 为不支持的环境提供兜底:退化为普通定位 */
.nav {
position: relative; /* 兜底值 */
}
@supports (position: sticky) {
.nav { position: sticky; top: 0; }
}
/* 或者用 JS 检测并降级到 fixed + 占位 */
const supportsSticky = CSS.supports('position', 'sticky');
if (!supportsSticky) {
/* 启用滚动监听降级方案 */
}
完整支持
完整支持
完整支持
完整支持
调试技巧:三步定位粘性失效
当你面对一个“明明写了却不生效”的 sticky 时,按下面的顺序排查,基本三分钟内就能定位问题。
position: sticky 是否被划掉(被覆盖)
position 最终值和 top 最终值
overflow 不为 visible 的
z-index 或背景透明导致“看起来没生效”
.debug-sticky {
outline: 2px solid #ef4444;
outline-offset: -2px;
}
/* 技巧 2:给父容器加背景,确认它的实际高度 */
.debug-parent {
background: rgba(37, 99, 235, 0.08);
outline: 1px dashed #2563eb;
}
/* 技巧 3:临时把 sticky 换成 relative,对比位置 */
/* 如果两者位置一样,说明 sticky 根本没生效 */
/* 技巧 4:用 JS 打印祖先链上的 overflow */
let el = document.querySelector('.sticky');
while (el && el !== document.body) {
const ov = getComputedStyle(el).overflow;
if (ov !== 'visible') console.log(el, ov);
el = el.parentElement;
}
!important、换 top 值、加 z-index,试图用“碰运气”的方式让它生效。
最佳实践清单
把全文的结论浓缩成一份可以直接贴在工位上的清单。
- 永远显式设置阈值:
top: 0或bottom: 0,没有阈值的sticky等同于relative - 祖先链保持 overflow: visible:把滚动交给真正的滚动容器,中间不要有截断层
- 保证父容器有活动空间:父容器高度必须大于粘性元素高度
- 显式设置 z-index:粘性元素必须能覆盖在内容之上,且不能被其他层盖住
- 背景必须不透明:否则下方内容透出,看起来像“没生效”
- 表格用 border-collapse: separate:并把粘性加在
th/td上 - 侧栏布局优先用 grid:天然等高,不需要额外处理 flex 的 stretch 问题
- 吸附反馈用 IntersectionObserver:哨兵元素方案比监听 scroll 更高效
- 只动画 transform 与 opacity:避免在粘性元素上做触发布局的动画
- 复杂场景用 contain 隔离:
contain: layout paint能显著降低重排范围 - 考虑安全区域:底部吸附栏别忘了
env(safe-area-inset-bottom) - 用 DevTools 可视化调试:先看清祖先链和高度,再改代码
/* 1. 顶部粘性导航 —— 最常用 */
.stick-nav {
position: sticky;
top: 0;
z-index: 100;
}
/* 2. 粘性侧边目录 —— 注意父列要等高 */
.stick-toc {
position: sticky;
top: 24px;
max-height: calc(100vh - 48px);
overflow-y: auto;
}
/* 3. 表格粘性表头 + 首列冻结 */
.stick-table thead th {
position: sticky;
top: 0;
z-index: 3;
}
.stick-table tbody td:first-child {
position: sticky;
left: 0;
z-index: 2;
}
/* 4. 分组吸顶标题 */
.stick-group-head {
position: sticky;
top: 0;
z-index: 5;
}
/* 5. 底部吸附操作栏 */
.stick-actions {
position: sticky;
bottom: 0;
z-index: 4;
padding-bottom: env(safe-area-inset-bottom, 0);
}
position: sticky 是 CSS 中少有的“看起来简单、用起来讲究”的属性。它不需要 JavaScript,不需要计算位置,却对 DOM 结构、布局方式、overflow 链有着明确的要求。理解了粘性约束矩形,你就理解了它的一切行为;满足了四个必要条件,它就会稳定地为你工作。
希望这篇长文能帮你彻底掌握粘性定位。如果你在项目中遇到过更刁钻的场景,欢迎回到上面的交互实验室,动手改一改属性值,观察变化——那比读十篇文章都管用。