定位(position)属性详解

张玥 2026年9月20日 阅读时间 30分钟
CSS position 布局 层叠上下文 z-index
前端开发技巧之定位(position)属性详解

如果说 Flexbox 和 Grid 解决的是“元素怎么排队”的问题,那么 position 解决的就是另一个维度的问题:元素可以离开队伍,去别的地方站着。它是 CSS 里最古老、最稳定、也最容易被误用的属性之一。写业务代码时,我们几乎每天都在用它——吸顶导航、悬浮按钮、下拉菜单、模态框、图片角标、加载遮罩、回到顶部,背后全部是 position。但也正因为太常用,很多人只记住了“absolute 是相对父元素定位”“fixed 是固定在屏幕上”这两句半对半错的口诀,一旦遇到定位跑偏、sticky 失效、z-index 怎么调都压不住的情况,就只能靠反复试数值来蒙。这篇文章会从文档流讲起,把 static / relative / absolute / fixed / sticky 五个取值逐个拆开,讲清楚包含块(containing block)、层叠上下文(stacking context)、偏移属性与性能代价,最后落到一整套可以直接复用的实战模板与排查清单上。整篇文章配了六个可以动手操作的交互演示,建议一边读一边点。

一、定位之前:先理解“文档流”这个坐标系

在学习 position 之前,必须先弄清楚一个前提:默认情况下,页面上的元素是怎么被摆放的?答案就是文档流(Normal Flow)。

文档流的规则很简单:块级元素从上到下依次堆叠,每个元素独占一行;行内元素从左到右排列,排不下就换行。每个元素都占据属于自己的那块空间,谁也不压着谁。你可以把它想象成一条正在排队的队伍——每个人站在自己的位置上,互不干扰。

position 这个属性做的事情,本质上就是改变某个元素在这套坐标系里的位置计算方式。它一共有五个取值:

1
static
默认值
老老实实待在文档流里。top/left/right/bottom 与 z-index 对它完全无效。绝大多数元素都是这个值。
2
relative
相对定位
相对自己原本的位置偏移,但原位置仍然占着空间。它最重要的用途其实是给子元素当“定位参照物”。
3
absolute
绝对定位
完全脱离文档流,其他元素会当它不存在。它相对最近的已定位祖先元素定位,找不到就一直往上找到初始包含块。
4
fixed
固定定位
相对浏览器视口定位,页面怎么滚动它都纹丝不动。右下角那个蓝色的“定位”按钮就是活的例子。
5
sticky
粘性定位
平时按文档流走,滚到设定的阈值(如 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 真正的主力用途:当“定位参照物”

在实际项目里,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 是定位家族里最强大、也最容易出问题的一个。它的核心规则有两条:

  1. 完全脱离文档流,原位置不再保留,后面的元素会补上来。
  2. 相对最近的已定位祖先元素的 padding box 定位;如果一路往上都找不到,就相对于初始包含块(可以粗略理解为视口大小的那个矩形)定位。

第 2 条是理解 absolute 的关键。下面这个实验台可以切换父元素的 position,观察橙色小方块“跳”到了哪里——这正是“寻找包含块”的过程。

父元素:position: static
absolute
.abs-stage(position: relative)

当父元素是 static 时,橙色方块越过了父元素,跑到外层 .abs-stage 的右上角——因为父元素“不够格”,它只能继续往上找。当父元素变成 relative 后,橙色方块立刻被“吸”回父元素的右上角。

典型 Bug
给子元素写了 position: absolute; top: 0; right: 0;,结果它飞到了整个页面的右上角。原因:所有祖先元素都是 static,它只能相对初始包含块定位。
解决方案
给最近的、你希望它参照的祖先元素加上 position: relative。注意是最近的,中间任何一层设置了 relative/absolute/fixed/sticky 都会截胡。

absolute 的定位原点:padding box,不是 content box

一个经常被忽略的细节:absolute 元素的偏移是相对包含块的 padding box 计算的,也就是包含边框但不包含外边距的那个矩形。这意味着如果父元素有 border,子元素的 top: 0 会贴着边框内侧,而不是边框外侧。

.parent {
  position: relative;
  border: 4px solid #2563eb;
  padding: 20px;
}

.child {
  position: absolute;
  top: 0;
  left: 0;
  /* 贴着边框内侧,而不是 padding 内边
     也不是 content 区域 */
}

两个经典的 absolute 用法

用法一:绝对居中。这是最古老也最稳的居中方案,兼容性极好:

.center {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

注意 translate(-50%, -50%) 里的百分比是相对元素自身尺寸的,这样才能把元素真正对准中心。top/left 用百分比则是相对包含块尺寸。这两个百分比含义不同,是初学者最容易混淆的地方。

用法二:撑满父容器。用 inset: 0 让绝对定位元素铺满整个包含块,常用于遮罩层、图片覆盖文字:

.overlay {
  position: absolute;
  inset: 0;  /* 等价于 top/right/bottom/left 都为 0 */
  background: rgba(15, 23, 42, 0.6);
}
inset: 24px

当 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 最经典的五个应用场景

顶部导航 滚动时始终吸在页面顶端(也可以用 sticky 实现,两者取舍见后文)
返回顶部 右下角常驻按钮,点击滚动回页面顶部
客服浮窗 在线咨询、意见反馈入口,不随内容滚动消失
模态遮罩 全屏遮罩层,锁住滚动、聚焦于弹窗内容
移动端底栏 底部 Tab 栏,配合 env(safe-area-inset-bottom) 适配刘海屏

重大陷阱:祖先元素上的 transform 会让 fixed 失效

这是 fixed 最著名的“坑”,也是无数开发者调试到崩溃的场景:只要 fixed 元素的任意一个祖先元素设置了 transform、filter、perspective、backdrop-filter 或 will-change(且值不是 none),这个祖先就会成为新的包含块,fixed 元素从此不再是相对视口定位,而是相对那个祖先定位。

更麻烦的是,很多 CSS 动画库、轮播组件、过渡效果都会给容器加上 transform。你可能什么都没改,只是引入了一个动画,右下角的按钮就突然跟着容器跑了。

症状
模态框、悬浮按钮、吸顶导航突然“不动了”,或者跟着某个容器一起滚走了。打开 DevTools 一层层往上找,会发现某个祖先带着 transform: translateZ(0) 之类的声明。
修复
把 fixed 元素移出那个被 transform 的祖先,挂到 <body> 直属下;或者改用 createPortal(React)/ Teleport(Vue)渲染到 body。

fixed 与移动端 100vh 问题

在移动端浏览器上,地址栏会随着滚动收起和展开,导致视口高度不断变化。给 fixed 元素写 height: 100vh 时,可能出现内容被地址栏挡住、或者底部留白的现象。现代方案是使用动态视口单位:

.fullscreen-overlay {
  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 生效的三个必要条件

  1. 必须指定阈值:至少要有 top、right、bottom、left 中的一个,且值不能是 auto。只写 position: sticky 是没有任何效果的——这是最高频的失效原因。
  2. 父容器必须有足够的高度:sticky 元素的粘性范围被限制在父容器的内容区域内。如果父容器和 sticky 元素一样高,那就没有可粘的空间,看起来就像完全没生效。
  3. 滚动祖先不能有 overflow: hidden:这会让滚动容器变成父元素本身,而它并不滚动,于是 sticky 也就无从粘起。注意 overflow: auto 或 scroll 是可以的,它会成为新的滚动祖先。
sticky 失效排查
① 忘了写 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 避免内容透出。
  • 侧边栏目录:文章目录滚动时跟随,到底部时自动停住。
  • 分组标题:通讯录、设置页里的分组标题,滚动时被下一个分组“顶走”,体验非常顺滑。
/* 吸顶导航:sticky 版 */
.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。也就是说:

<!-- 这个结构里,.child 会相对 .middle 定位,而不是 .outer -->
<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 会被忽略(在从左到右的书写模式下)。
/* 拉伸:宽度会填满父容器(减去两侧各 20px) */
.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 完全一致:

.box {
  /* 四边都是 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

上面这个绿色方块使用了 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 时,浏览器按以下顺序从下往上绘制:

1. 背景与边框(根元素)
2. 负 z-index 的定位元素
3. 块级盒子 / 浮动 / 行内内容
4. 正 z-index 的定位元素

一个常见的细节:定位元素默认绘制在普通流内容之上。所以给一个 absolute 元素不设 z-index,它依然会盖住周围的普通文本。

层叠上下文:z-index 的真正“隔离墙”

这是 z-index 最难理解、也最容易引发 bug 的部分。所谓层叠上下文(Stacking Context),可以理解为一个独立的“图层组”:在这个组内部的元素,无论 z-index 多大,都无法超越组与组之间的层级关系。

换句话说:子元素的 z-index 只在同一个层叠上下文内部比较,跨上下文时比较的是上下文本身的层级。

下面这个实验台可以点击色块把它提到最前面,观察 z-index 的实际效果:

z-index: 1
z-index: 2
z-index: 3

点击任意色块,它的 z-index 会被提升到 10,从而覆盖在另外两个之上。

哪些属性会创建层叠上下文

层叠上下文并不只由 z-index 触发。以下情况都会创建新的层叠上下文(列表在持续增长):

  • 根元素 <html>
  • position: absolute / relative 且 z-index 不为 auto
  • position: fixed 或 sticky(无论 z-index 是多少)
  • flex / grid 容器的直接子项且 z-index 不为 auto
  • opacity 小于 1
  • transform、filter、perspective、backdrop-filter 不为 none
  • mix-blend-mode 不为 normal
  • isolation: isolate
  • will-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 所在的上下文高。
正解
用 DevTools 的 Layers 面板观察层叠结构,找到冲突的两个层叠上下文,调整容器级别的 z-index,而不是一味加大子元素的数值。

工程化的 z-index 管理

中大型项目里,随手写 z-index: 999 是灾难的开始。推荐做法是用 CSS 自定义属性建立一套层级刻度:

:root {
  --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); }

这样每个人都知道“弹窗该用哪一档”,不用再猜数字,也避免了不同模块之间为了压过对方而无休止地加码。

十、实战:六个可以直接抄的定位模板

模板一:图片角标(父相子绝)

<div class="thumb">
  <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;
}

模板二:下拉菜单

.dropdown {
  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 则无法参与过渡动画。

模板三:模态框

.modal-backdrop {
  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 元素就会失效。

模板四:吸顶导航

.site-header {
  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 做出毛玻璃效果时要注意:它会创建层叠上下文,也会带来一定的渲染开销。在低端设备上大面积使用可能掉帧,移动端慎用。

模板五:回到顶部按钮

.back-to-top {
  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,避免透明按钮挡住点击 */

模板六:长列表分组标题

.group-title {
  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 却还是不生效”排查流程

  1. 确认元素 position 不是 static(或者是 flex / grid 的直接子项)。
  2. 确认 z-index 写的是数字,不是字符串,也不是 auto。
  3. 检查两个冲突元素是否在同一个层叠上下文中——如果不是,比较它们的上下文容器的 z-index。
  4. 检查祖先有没有 transform、opacity < 1、filter 等意外创建上下文的属性。
  5. 用 DevTools 的 Layers 面板可视化层叠结构,比猜数值快得多。

“sticky 不生效”排查流程

  1. 有没有设置 top / bottom / left / right?没有阈值就不会粘。
  2. 父容器的高度是否大于 sticky 元素?没有可粘的空间就不会粘。
  3. 某个祖先是否有 overflow: hidden?它会让 sticky 失去滚动参照。
  4. 父容器是不是 flex 容器且没有设置 align-self?默认的 stretch 会让 sticky 元素被拉满,失去粘性效果。
  5. 有没有其他样式(比如某个通用选择器)覆盖了 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 在哪个层叠上下文里比较?它的偏移会不会触发重排?把这四个问题答清楚,定位就不再是“试试看能不能对上”,而是一件可以被推理、被预测的事情。

position: fixed

我相对视口固定,页面滚到哪里我都在这儿。

scrollY0
视口高度0
right / bottom24px