如果说 Flexbox 和 Grid 解决的是“元素怎么排队”的问题,那么 position 解决的就是另一个维度的问题:元素可以离开队伍,去别的地方站着。它是 CSS 里最古老、最稳定、也最容易被误用的属性之一。写业务代码时,我们几乎每天都在用它——吸顶导航、悬浮按钮、下拉菜单、模态框、图片角标、加载遮罩、回到顶部,背后全部是 position。但也正因为太常用,很多人只记住了“absolute 是相对父元素定位”“fixed 是固定在屏幕上”这两句半对半错的口诀,一旦遇到定位跑偏、sticky 失效、z-index 怎么调都压不住的情况,就只能靠反复试数值来蒙。这篇文章会从文档流讲起,把 static / relative / absolute / fixed / sticky 五个取值逐个拆开,讲清楚包含块(containing block)、层叠上下文(stacking context)、偏移属性与性能代价,最后落到一整套可以直接复用的实战模板与排查清单上。整篇文章配了六个可以动手操作的交互演示,建议一边读一边点。
一、定位之前:先理解“文档流”这个坐标系
在学习 position 之前,必须先弄清楚一个前提:默认情况下,页面上的元素是怎么被摆放的?答案就是文档流(Normal Flow)。
文档流的规则很简单:块级元素从上到下依次堆叠,每个元素独占一行;行内元素从左到右排列,排不下就换行。每个元素都占据属于自己的那块空间,谁也不压着谁。你可以把它想象成一条正在排队的队伍——每个人站在自己的位置上,互不干扰。
position 这个属性做的事情,本质上就是改变某个元素在这套坐标系里的位置计算方式。它一共有五个取值:
top/left/right/bottom 与 z-index 对它完全无效。绝大多数元素都是这个值。top: 0)就“粘”住。它是 relative 和 fixed 的混合体,但受父容器范围限制。这五个值可以按“是否脱离文档流”分成两类,这是理解定位的第一把钥匙:
| 取值 | 是否脱离文档流 | 定位参照物 | 是否创建层叠上下文 |
|---|---|---|---|
static |
不脱离 | 无(忽略偏移属性) | 否 |
relative |
不脱离 | 自身原本的位置 | 是(z-index 非 auto 时) |
absolute |
脱离 | 最近的已定位祖先 | 是(z-index 非 auto 时) |
fixed |
脱离 | 视口(或被 transform 的祖先) | 是(始终) |
sticky |
不脱离 | 最近的滚动祖先 | 是(始终) |
“脱离文档流”到底意味着什么?意味着这个元素不再占据原本的空间,后面的兄弟元素会直接往上补位,就像它从来没出现过一样。这也是很多人第一次用 absolute 时最惊讶的地方——明明只是想让一个图标挪个位置,结果整个布局塌了。
absolute 就是相对于父元素定位。”——不准确。它是相对于最近的、position 不为 static 的祖先元素定位。如果父元素是默认的 static,它会继续往上找。
absolute 相对于最近的已定位祖先的 padding box 定位;若不存在,则相对于初始包含块(即视口大小的矩形)定位。”
二、static:默认值,也是一切参照系的起点
position: static 是所有元素的初始值。它意味着:这个元素老老实实地待在文档流里,不参与任何花哨的定位。
它的行为特征非常明确:
- 偏移属性无效:写
top: 50px、left: 100px完全不起作用,浏览器会直接忽略。 z-index无效:在 static 元素上设置z-index不会有任何效果。这一点在层叠上下文那一节还会再强调。- 仍然可以作为参照物:虽然 static 元素自己不能定位,但它仍然占据空间,仍然是子元素文档流的一部分。
为什么很多人觉得 static “没用”?因为它太普通了,普通到你根本感觉不到它的存在。但实际上,整个文档流就是建立在 static 之上的。定位元素之所以能“跑出去”,恰恰是因为有一个稳定的、不动的背景板作为对比。
.box {
position: static;
}
.box { }
/* 下面这些属性在 static 元素上全部无效 */
.box {
position: static;
top: 20px; /* 无效 */
left: 30px; /* 无效 */
z-index: 999; /* 无效 */
}
有一个例外值得记住:如果元素是 flex 或 grid 容器的直接子项,那么即使 position: static,z-index 也会生效。这是 CSS 规范里的特殊规定,也是不少“玄学层叠问题”的源头。不过这是 z-index 的议题,我们在第九节会详细展开。
三、relative:不脱离文档流的“微调”
position: relative 的规则只有一句话:相对于它自己原本应该在的位置进行偏移,但原本的空间依然被保留。
这句话包含两个关键信息:
- 参照物是自己,不是父元素,也不是视口。写
top: 20px就是“从原来位置向下挪 20px”。 - 它没有脱离文档流。原来占的那块地方还留着,周围的元素不会动,所以偏移后它可能会盖住邻居。
下面这个实验台可以拖动滑块,实时观察 top 与 left 的变化。注意虚线框——那是它的“原位置”,蓝色方块跑得再远,虚线框也一直留在原地,因为空间从未被释放。
relative 真正的主力用途:当“定位参照物”
在实际项目里,relative 被用得最多的地方,并不是挪动自己,而是给子元素提供一个定位基准。经典的组合写法是:
.card {
position: relative;
}
/* 子元素:绝对定位到父元素的右上角 */
.card .badge {
position: absolute;
top: -8px;
right: -8px;
}
这种“父相子绝”(父元素 relative、子元素 absolute)的写法,几乎是所有角标、关闭按钮、悬浮操作条的标配。父元素虽然写了 position: relative,但因为四个偏移属性都是默认的 auto,所以它的视觉位置一点没变——它只是安静地当了个坐标系。
三个容易踩的坑
- 用 relative 做位移:如果你的目的只是让元素动一下,优先考虑
transform: translate()或margin。relative 偏移不会触发重排,但会让布局关系变得难以推理。 - 忘记它会创建层叠上下文:一旦给 relative 元素设了
z-index(非 auto),它就变成了一个新的层叠上下文,子元素的 z-index 从此只在内部比较,再也不能和外部元素“掰手腕”。 - 叠加在兄弟元素之上却看不出来:relative 偏移后可能盖住相邻元素,但因为没设 z-index,实际谁在上面取决于文档顺序,容易出现“同样的代码,换个位置表现就不同”的困惑。
四、absolute:脱离文档流,寻找“包含块”
position: absolute 是定位家族里最强大、也最容易出问题的一个。它的核心规则有两条:
- 完全脱离文档流,原位置不再保留,后面的元素会补上来。
- 相对最近的已定位祖先元素的 padding box 定位;如果一路往上都找不到,就相对于初始包含块(可以粗略理解为视口大小的那个矩形)定位。
第 2 条是理解 absolute 的关键。下面这个实验台可以切换父元素的 position,观察橙色小方块“跳”到了哪里——这正是“寻找包含块”的过程。
当父元素是 static 时,橙色方块越过了父元素,跑到外层 .abs-stage 的右上角——因为父元素“不够格”,它只能继续往上找。当父元素变成 relative 后,橙色方块立刻被“吸”回父元素的右上角。
position: absolute; top: 0; right: 0;,结果它飞到了整个页面的右上角。原因:所有祖先元素都是 static,它只能相对初始包含块定位。
position: relative。注意是最近的,中间任何一层设置了 relative/absolute/fixed/sticky 都会截胡。
absolute 的定位原点:padding box,不是 content box
一个经常被忽略的细节:absolute 元素的偏移是相对包含块的 padding box 计算的,也就是包含边框但不包含外边距的那个矩形。这意味着如果父元素有 border,子元素的 top: 0 会贴着边框内侧,而不是边框外侧。
position: relative;
border: 4px solid #2563eb;
padding: 20px;
}
.child {
position: absolute;
top: 0;
left: 0;
/* 贴着边框内侧,而不是 padding 内边
也不是 content 区域 */
}
两个经典的 absolute 用法
用法一:绝对居中。这是最古老也最稳的居中方案,兼容性极好:
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
注意 translate(-50%, -50%) 里的百分比是相对元素自身尺寸的,这样才能把元素真正对准中心。top/left 用百分比则是相对包含块尺寸。这两个百分比含义不同,是初学者最容易混淆的地方。
用法二:撑满父容器。用 inset: 0 让绝对定位元素铺满整个包含块,常用于遮罩层、图片覆盖文字:
position: absolute;
inset: 0; /* 等价于 top/right/bottom/left 都为 0 */
background: rgba(15, 23, 42, 0.6);
}
当 top 和 bottom 同时被指定,且元素的 height 是 auto 时,元素会被拉伸到同时满足这两个值——这正是 inset: 0 能撑满容器的原理。同理,同时指定 left 与 right 且 width: auto 时,宽度会被拉伸。
五、fixed:相对视口固定,页面滚它不滚
position: fixed 的规则非常直白:相对浏览器视口定位,无论页面怎么滚动,它都待在原地。它同样脱离文档流,不占空间。
你现在就能看到活生生的例子——页面右下角那个蓝色的“定位”按钮,就是 position: fixed。试着滚动页面,它会一直待在那个角落;点一下,它还会展开一个面板,实时显示当前的 scrollY 和视口高度。
.fixed-widget {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 150;
}
fixed 最经典的五个应用场景
env(safe-area-inset-bottom) 适配刘海屏
重大陷阱:祖先元素上的 transform 会让 fixed 失效
这是 fixed 最著名的“坑”,也是无数开发者调试到崩溃的场景:只要 fixed 元素的任意一个祖先元素设置了 transform、filter、perspective、backdrop-filter 或 will-change(且值不是 none),这个祖先就会成为新的包含块,fixed 元素从此不再是相对视口定位,而是相对那个祖先定位。
更麻烦的是,很多 CSS 动画库、轮播组件、过渡效果都会给容器加上 transform。你可能什么都没改,只是引入了一个动画,右下角的按钮就突然跟着容器跑了。
transform: translateZ(0) 之类的声明。
<body> 直属下;或者改用 createPortal(React)/ Teleport(Vue)渲染到 body。
fixed 与移动端 100vh 问题
在移动端浏览器上,地址栏会随着滚动收起和展开,导致视口高度不断变化。给 fixed 元素写 height: 100vh 时,可能出现内容被地址栏挡住、或者底部留白的现象。现代方案是使用动态视口单位:
position: fixed;
inset: 0;
/* 100dvh 会随地址栏变化动态调整 */
height: 100dvh;
}
/* 不支持 dvh 的浏览器用 svh / vh 兜底 */
.fullscreen-overlay {
height: 100vh;
height: 100dvh;
}
相关单位还有 svh(最小视口高度,地址栏展开时的高度)和 lvh(最大视口高度)。做全屏遮罩、固定底栏时,用 dvh 通常最贴合用户实际看到的效果。
六、sticky:粘性定位,relative 与 fixed 的混合体
position: sticky 是 HTML5 时代新增的取值,它的行为可以概括成一句话:元素在滚动到设定的阈值之前,表现得像 relative;到达阈值之后,表现得像 fixed——但只在父容器的范围内。
下面是一个可直接滚动的演示容器,橙色横条设置了 position: sticky; top: 0。试着在容器内滚动,观察它什么时候粘住、什么时候被“推走”。
第一段内容:滚动这个容器,观察上方横条的行为。
第二段内容:当横条到达容器顶部时,它会停止移动,看起来像被“粘”住了。
第三段内容:继续滚动,横条会一直停留在容器顶部。
第四段内容:当容器底部接近时,横条会被推着一起滚出去。
第五段内容:这就是 sticky 与 fixed 最大的区别——它不会脱离父容器的范围。
第六段内容:用鼠标滚轮或触控板在这个区域内滚动试试。
第七段内容:继续往下滚,横条最终会被“顶”出可视区域。
第八段内容:这是最后一段了,横条此时应该已经离开了容器顶部。
sticky 生效的三个必要条件
- 必须指定阈值:至少要有
top、right、bottom、left中的一个,且值不能是auto。只写position: sticky是没有任何效果的——这是最高频的失效原因。 - 父容器必须有足够的高度:sticky 元素的粘性范围被限制在父容器的内容区域内。如果父容器和 sticky 元素一样高,那就没有可粘的空间,看起来就像完全没生效。
- 滚动祖先不能有
overflow: hidden:这会让滚动容器变成父元素本身,而它并不滚动,于是 sticky 也就无从粘起。注意overflow: auto或scroll是可以的,它会成为新的滚动祖先。
top 等阈值;② 父元素高度不够;③ 祖先有 overflow: hidden;④ 祖先有 display: flex 且子项被拉伸;⑤ 元素自身设了 position: relative 覆盖了 sticky。
.header { position: sticky; top: 0; z-index: 10; }别忘了 z-index——sticky 元素经常需要压在内容之上,否则会被后面的内容盖住。
sticky 的经典应用
- 吸顶导航:相比 fixed,sticky 不需要给主体内容补一个等高 padding,也不会脱离文档流,布局更自然。
- 表格表头:长表格滚动时让
thead th保持可见,配合background避免内容透出。 - 侧边栏目录:文章目录滚动时跟随,到底部时自动停住。
- 分组标题:通讯录、设置页里的分组标题,滚动时被下一个分组“顶走”,体验非常顺滑。
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
}
/* 长表格的粘性表头 */
.data-table thead th {
position: sticky;
top: 0;
background: #f8fafc;
z-index: 1;
}
sticky 还是 fixed?一个简单的判断标准
| 维度 | position: fixed |
position: sticky |
|---|---|---|
| 是否脱离文档流 | 是 | 否 |
| 是否需要在主体加 padding | 需要(否则内容被遮住) | 不需要 |
| 作用范围 | 整个视口 | 父容器范围内 |
| 是否受 transform 影响 | 会(祖先 transform 会改变参照物) | 不受此影响 |
| 推荐场景 | 全局浮层、模态、悬浮按钮 | 吸顶导航、表头、侧边目录 |
七、包含块(Containing Block)完全指南
“包含块”是定位体系里最重要的概念,没有之一。所谓包含块,就是一个元素在计算百分比尺寸和偏移位置时所参照的那个矩形。不同 position 值下,包含块的确定规则完全不同:
| 元素的 position | 包含块是 |
|---|---|
static 或 relative |
最近的块级祖先的 content box(内容区域,不含 padding 和 border) |
absolute |
最近的 position 不为 static 的祖先的 padding box |
fixed |
视口(除非祖先有 transform / filter / perspective / will-change) |
sticky |
与 static / relative 相同:最近的块级祖先的 content box |
注意这里的细节差异:absolute 用的是 padding box,而 static / relative 用的是 content box。这意味着同一个父元素,作为 relative 子元素的参照物和作为 absolute 子元素的参照物时,尺寸和原点是不一样的。
“已定位祖先”到底指什么
很多人以为只有 relative 才算“已定位”。实际上,只要 position 不是 static,都算已定位,包括 absolute、fixed、sticky。也就是说:
<div class="outer" style="position: relative">
<div class="middle" style="position: absolute">
<div class="child" style="position: absolute; top: 0; right: 0"></div>
</div>
</div>
这也是为什么在复杂组件里,absolute 元素经常会“认错爹”。中间任何一层无意的 position: relative,都会截断查找链。调试时的正确姿势是:选中元素,在 DevTools 里一层层往上翻,找到第一个 position 不是 static 的祖先。
初始包含块
如果一路往上找到 <html> 都没有找到已定位祖先,absolute 元素就会相对初始包含块定位。初始包含块的尺寸等于视口大小,位置固定在文档原点,但它不随滚动移动——这一点和 fixed 不同。所以一个没有已定位祖先的 absolute 元素,会随着页面滚动而离开视野。
body { position: relative; } 想让所有 absolute 元素都相对 body 定位。风险:body 的高度通常由内容撑开,滚动后参照物会跟着变,行为难以预测。
position: relative。范围越明确,行为越可预测。
八、偏移属性:top / right / bottom / left 与 inset
四个偏移属性只在元素 position 不为 static 时才生效。它们的取值规则看似简单,实际上有几个反直觉的地方。
取值的含义
auto(默认值):浏览器自动计算,通常等于“待在原来的位置”。<length>:如20px、2rem,相对包含块对应边的距离。<percentage>:相对包含块的宽(left/right)或高(top/bottom)。注意:top/bottom 的百分比是相对包含块高度,如果包含块高度是 auto,百分比可能解析为 0。- 负值:允许,表示往反方向偏移。角标“溢出”父元素的效果就靠负值实现。
同时指定两个对立属性会怎样
这是 absolute 的一个隐藏能力。当同一轴上的两个值都被指定且不为 auto 时:
- 如果元素的
width/height是auto,元素会被拉伸以同时满足两端。 - 如果元素尺寸已经确定,那么以
left/top为准,right/bottom会被忽略(在从左到右的书写模式下)。
.stretch {
position: absolute;
left: 20px;
right: 20px;
width: auto; /* 关键 */
}
/* 定宽居中:同时写 left/right 和 margin: auto 可以水平居中 */
.centered {
position: absolute;
left: 0;
right: 0;
width: 300px;
margin: 0 auto;
}
inset:一次写四个方向
inset 是四个偏移属性的简写,语法和 margin / padding 完全一致:
/* 四边都是 0,铺满包含块 */
inset: 0;
}
.box-2 {
/* 上下 10px,左右 20px */
inset: 10px 20px;
}
.box-3 {
/* 上 10px,左右 20px,下 30px */
inset: 10px 20px 30px;
}
.box-4 {
/* 分别对应 inset-block-start / inline-end / block-end / inline-start */
inset: 10px 20px 30px 40px;
}
还有对应的逻辑属性:inset-block-start、inset-block-end、inset-inline-start、inset-inline-end。它们会随书写模式和文字方向自动翻转,做国际化项目时非常有用。
上面这个绿色方块使用了 inset: 0; margin: auto; width: 120px; height: 74px;——在绝对定位下,margin: auto 可以同时在水平和垂直方向实现居中,而且不需要知道父容器的尺寸。这是绝对定位居中的第二种经典写法。
九、z-index 与层叠上下文
定位元素一旦重叠,就必然涉及“谁在上面”的问题。z-index 就是回答这个问题的属性,但它只在特定条件下生效,而且经常出现“设了 9999 还是被压住”的情况。
z-index 生效的前提
z-index 只对已定位元素(position 不为 static)或 flex / grid 容器的直接子项生效。普通 static 元素写 z-index: 9999 是徒劳的。
没有 z-index 时的层叠顺序
当多个元素重叠且都没有设置 z-index 时,浏览器按以下顺序从下往上绘制:
一个常见的细节:定位元素默认绘制在普通流内容之上。所以给一个 absolute 元素不设 z-index,它依然会盖住周围的普通文本。
层叠上下文:z-index 的真正“隔离墙”
这是 z-index 最难理解、也最容易引发 bug 的部分。所谓层叠上下文(Stacking Context),可以理解为一个独立的“图层组”:在这个组内部的元素,无论 z-index 多大,都无法超越组与组之间的层级关系。
换句话说:子元素的 z-index 只在同一个层叠上下文内部比较,跨上下文时比较的是上下文本身的层级。
下面这个实验台可以点击色块把它提到最前面,观察 z-index 的实际效果:
点击任意色块,它的 z-index 会被提升到 10,从而覆盖在另外两个之上。
哪些属性会创建层叠上下文
层叠上下文并不只由 z-index 触发。以下情况都会创建新的层叠上下文(列表在持续增长):
- 根元素
<html> position: absolute / relative且z-index不为autoposition: fixed或sticky(无论 z-index 是多少)- flex / grid 容器的直接子项且
z-index不为auto opacity小于 1transform、filter、perspective、backdrop-filter不为 nonemix-blend-mode不为normalisolation: isolatewill-change指定了上述任一属性contain: layout / paint / strict / content- 容器查询
container-type为size或inline-size
这正是“为什么我 z-index 设了 9999 还是被盖住”最常见的答案:你被压住的元素和压住它的元素,根本不在同一个层叠上下文里。你要调的不是子元素的 z-index,而是它们各自所属的上下文容器的 z-index。
.modal { position: fixed; z-index: 9999; } 却依然被某个下拉菜单遮住。原因是下拉菜单的祖先有一个 transform,而这个祖先的 z-index 比 modal 所在的上下文高。
工程化的 z-index 管理
中大型项目里,随手写 z-index: 999 是灾难的开始。推荐做法是用 CSS 自定义属性建立一套层级刻度:
--z-base: 1;
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-toast: 600;
--z-tooltip: 700;
}
.dropdown { z-index: var(--z-dropdown); }
.site-header { z-index: var(--z-sticky); }
.modal { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); }
这样每个人都知道“弹窗该用哪一档”,不用再猜数字,也避免了不同模块之间为了压过对方而无休止地加码。
十、实战:六个可以直接抄的定位模板
模板一:图片角标(父相子绝)
<img src="cover.avif" alt="封面">
<span class="badge">NEW</span>
</div>
.thumb {
position: relative;
overflow: hidden;
border-radius: 8px;
}
.thumb img {
display: block;
width: 100%;
}
.thumb .badge {
position: absolute;
top: 8px;
left: 8px;
background: #ef4444;
color: #fff;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
}
模板二:下拉菜单
position: relative;
display: inline-block;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 6px); /* 紧贴触发器下方,留 6px 间距 */
left: 0;
min-width: 180px;
z-index: var(--z-dropdown);
opacity: 0;
visibility: hidden;
transform: translateY(-6px);
transition: all 0.2s ease;
}
.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
注意 :focus-within——它让键盘用户 Tab 到触发器时也能展开菜单,是纯 CSS 下拉菜单保持无障碍的关键。visibility 配合 opacity 过渡,可以保证隐藏时元素不可被点击,而 display: none 则无法参与过渡动画。
模板三:模态框
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.55);
z-index: var(--z-modal-backdrop);
display: grid;
place-items: center;
}
.modal-panel {
position: relative; /* 给内部关闭按钮当参照物 */
width: min(560px, 90vw);
max-height: 80dvh;
overflow: auto;
background: #fff;
border-radius: 12px;
}
.modal-close {
position: absolute;
top: 12px;
right: 12px;
}
这里用 display: grid; place-items: center 让面板居中,比传统的 transform: translate(-50%, -50%) 更简洁。但要记住:一旦在 backdrop 上用了 transform 做居中,内部的 fixed 元素就会失效。
模板四:吸顶导航
position: sticky;
top: 0;
z-index: var(--z-sticky);
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid #e2e8f0;
}
用 backdrop-filter 做出毛玻璃效果时要注意:它会创建层叠上下文,也会带来一定的渲染开销。在低端设备上大面积使用可能掉帧,移动端慎用。
模板五:回到顶部按钮
position: fixed;
right: 24px;
bottom: calc(24px + env(safe-area-inset-bottom));
z-index: var(--z-fixed);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.back-to-top.is-visible {
opacity: 1;
pointer-events: auto;
}
/* 隐藏时同时关掉 pointer-events,避免透明按钮挡住点击 */
模板六:长列表分组标题
position: sticky;
top: 0;
z-index: 1;
background: #f1f5f9;
padding: 8px 16px;
font-weight: 600;
}
每个分组标题在滚动到顶部时会粘住,被下一个分组标题推走,形成“接力”效果。通讯录、设置页、聊天记录的时间分隔都用这个套路。关键点是背景色必须是不透明的,否则下面的内容会透出来。
十一、性能与常见陷阱
动画应该用 transform,而不是 top / left
改变 top / left 会触发浏览器的布局(Layout)阶段,浏览器需要重新计算元素位置、重排周围内容,然后重新绘制、重新合成。而 transform 只影响合成(Composite)阶段,可以直接交给 GPU 处理。
| 方案 | 触发阶段 | 性能 | 建议 |
|---|---|---|---|
top / left / right / bottom |
Layout → Paint → Composite | 较差 | 只用于静态定位,不做动画 |
margin |
Layout → Paint → Composite | 较差 | 避免用于动画 |
transform: translate() |
Composite | 优秀 | 位移动画首选 |
opacity |
Composite | 优秀 | 淡入淡出首选 |
.toast {
position: fixed;
bottom: 24px;
transition: bottom 0.3s ease;
}
.toast.hide { bottom: -80px; }
/* 推荐:只走合成层 */
.toast {
position: fixed;
bottom: 24px;
transform: translateY(0);
transition: transform 0.3s ease, opacity 0.3s ease;
}
.toast.hide {
transform: translateY(120%);
opacity: 0;
}
“设了 z-index 却还是不生效”排查流程
- 确认元素
position不是static(或者是 flex / grid 的直接子项)。 - 确认
z-index写的是数字,不是字符串,也不是auto。 - 检查两个冲突元素是否在同一个层叠上下文中——如果不是,比较它们的上下文容器的 z-index。
- 检查祖先有没有
transform、opacity < 1、filter等意外创建上下文的属性。 - 用 DevTools 的 Layers 面板可视化层叠结构,比猜数值快得多。
“sticky 不生效”排查流程
- 有没有设置
top/bottom/left/right?没有阈值就不会粘。 - 父容器的高度是否大于 sticky 元素?没有可粘的空间就不会粘。
- 某个祖先是否有
overflow: hidden?它会让 sticky 失去滚动参照。 - 父容器是不是 flex 容器且没有设置
align-self?默认的stretch会让 sticky 元素被拉满,失去粘性效果。 - 有没有其他样式(比如某个通用选择器)覆盖了
position?用 DevTools 看计算样式最直接。
其他容易忽略的细节
- 绝对定位元素的可访问顺序:DOM 顺序决定 Tab 顺序。如果为了视觉效果把关闭按钮放在 DOM 最后,键盘用户要按很多次 Tab 才能到它。能用
order或视觉技巧解决的,不要随意打乱 DOM。 - 定位元素的百分比高度:如果包含块高度是
auto,top: 50%往往解析不出预期值。给包含块一个确定高度,或者改用inset: 0; margin: auto的方案。 - 滚动容器内的 fixed:fixed 元素不会随滚动容器滚动,如果它本该“待在某个滚动区域里”,请改用 absolute 或 sticky。
- 打印样式:fixed 元素在打印时会出现在每一页。做打印友好的页面时,记得在
@media print里把悬浮组件隐藏掉。
@media print {
.fixed-widget,
.back-to-top,
.modal-backdrop {
display: none !important;
}
}
十二、定位使用检查清单
把全文的要点浓缩成一份可以贴在工位上的清单。每次写完涉及定位的样式,扫一眼,能挡掉绝大多数问题。
- 能不动用定位就不动用:能用 Flexbox / Grid 解决的布局问题,不要用
absolute硬凑。 - “父相子绝”要精确到最近的容器:给哪个父元素加
relative,取决于你想让子元素相对谁定位。 - absolute 元素写完后,检查它是否脱离文档流影响了后面元素:必要时用
margin或占位元素补回空间。 - fixed 元素要检查祖先有没有 transform / filter / will-change:有的话,把 fixed 元素提到 body 下。
- sticky 必须写阈值,且父容器要高够:同时确认没有祖先的
overflow: hidden截断。 - 层叠问题先看上下文,再看 z-index:跨上下文比较数值是无效的。
- 用 CSS 变量统一管理 z-index:不要散落
999、9999这样的魔法数字。 - 位移动画用 transform,不用 top / left:避免每帧触发重排。
- 全屏遮罩用
inset: 0+dvh:兼顾代码简洁与移动端地址栏问题。 - 注意安全区域:固定底栏用
env(safe-area-inset-bottom)避开刘海屏和手势条。 - 检查 DOM 顺序是否合理:定位不会改变 Tab 顺序,视觉顺序与键盘顺序不一致会伤害无障碍。
- 打印样式里隐藏悬浮组件:避免每一页都印上一个“返回顶部”。
:root {
--z-sticky: 200;
--z-fixed: 300;
--z-modal: 500;
}
/* 悬浮层:统一固定在右下角,并避开安全区 */
.floating-layer {
position: fixed;
right: 24px;
bottom: calc(24px + env(safe-area-inset-bottom));
z-index: var(--z-fixed);
}
/* 遮罩:铺满视口,兼容移动端地址栏 */
.overlay {
position: fixed;
inset: 0;
height: 100dvh;
z-index: var(--z-modal);
}
/* 吸顶:优先 sticky,而不是 fixed */
.sticky-header {
position: sticky;
top: 0;
z-index: var(--z-sticky);
background: #fff;
}
定位是 CSS 里最“古老”的一块内容,但它从来没有过时。Flexbox 和 Grid 让绝大多数“排队问题”变得简单,而 position 依然负责那些无法用排队解决的问题——让元素脱离队伍,去它该去的地方。
真正理解定位的标志,不是背下五个取值的定义,而是能够回答这几个问题:这个元素脱离文档流了吗?它的包含块是谁?它的 z-index 在哪个层叠上下文里比较?它的偏移会不会触发重排?把这四个问题答清楚,定位就不再是“试试看能不能对上”,而是一件可以被推理、被预测的事情。