写在前面
CSS 布局的三块基石是:文档流(normal flow)、浮动(float)、定位(position)。前两篇我们聊了 Flex 和 Grid——它们解决的是"元素如何排布"。而 position 解决的是另一个问题:元素如何脱离正常排布,精确地"钉"在页面的某个位置。
position 一共有五个值:static、relative、absolute、fixed、sticky。很多同学背得下每个值的含义,但一写就翻车——最常见的原因是没有真正理解两个底层概念:包含块(containing block) 和 文档流。这篇文章会先把这两个地基打牢,再逐个拆解五个取值,最后用实战案例和踩坑清单收尾。
先记住一句话总纲:
position 决定"元素相对于谁、以什么方式定位";top/right/bottom/left 决定"偏移多少";z-index 决定"堆叠时谁在上"。
一、前置知识:文档流与包含块
1.1 普通文档流
不设置任何定位和浮动的元素,会按照 HTML 顺序从上到下、从左到右排列,这叫普通文档流。绝大多数元素的默认状态就是如此。
position 的关键作用之一是改变元素是否脱离文档流:
- 不脱离文档流:元素仍占据原来的位置,后续元素不受影响(
static、relative、sticky)。 - 脱离文档流:元素从文档流中"抽走",不占位,后续元素会顶上来(
absolute、fixed)。
1.2 包含块(containing block)——定位的"坐标系"
偏移属性 top/right/bottom/left 是相对"包含块"来计算的。包含块不等于父元素,这是新手最大的误区。规则如下:
| 元素定位方式 | 它的包含块 |
|---|---|
static / relative | 最近的块级祖先(内容区) |
absolute | 最近的 position 非 static 的祖先;若无,则是初始包含块(通常是视口/html) |
fixed | 视口(viewport);除非祖先有 transform/filter/perspective 等属性,此时包含块变成该祖先 |
sticky | 最近的滚动祖先(viewport 或设置了 overflow 的元素) |
划重点:absolute 的包含块不是它的父元素,而是"最近的定位祖先"(position 为 relative/absolute/fixed/sticky 的祖先)。 这就是为什么"给父元素加 relative,让子元素 absolute 相对它定位"是固定搭配——其实父元素用什么非 static 值都可以,只是 relative 不会脱离文档流、最省事。
/* 经典搭配:父 relative + 子 absolute */
.parent { position: relative; }
.child { position: absolute; top: 10px; left: 10px; }
/* child 的 top/left 相对 parent 计算 */
二、position: static(默认值)
static 是 position 的默认值,表示"正常文档流定位"。
.element {
position: static;
}
要点:
- 元素在文档流中按顺序排列,
top/right/bottom/left、z-index对它完全无效。 - 不需要写,因为这是默认值;但如果想"撤销"某个元素上继承/命中的定位(比如重置第三方组件样式),显式写
position: static即可。
三、position: relative(相对定位)
relative 是"相对自己原来的位置"偏移。
.box {
position: relative;
top: 20px; /* 向下移 20px(相对原位置) */
left: -10px; /* 向左移 10px */
}
<div class="wrap">
<div class="box" style="position: relative; top: 30px; left: 30px;">相对定位</div>
<div class="sibling">后面的兄弟元素</div>
</div>
特点:
- 不脱离文档流:元素原来的占位保留,后面的元素不会被顶上来(只是视觉上挪了位置,可能盖住兄弟元素)。
- 偏移相对自身的原位置,而不是父元素或视口。
- 设置
relative的元素会成为absolute后代的定位祖先(最常用的用途)。 - 配合
z-index可以提升层叠顺序。
最常见的用途:微调位置(图标对齐、文字微位移),以及给绝对定位子元素当定位基准。
/* 微调:让按钮图标相对文字垂直居中微调 2px */
.icon { position: relative; top: 2px; }
四、position: absolute(绝对定位)
absolute 让元素脱离文档流,相对最近的定位祖先定位;如果往上找不到任何定位祖先,就相对初始包含块(视口/html 根)定位。
.badge {
position: absolute;
top: -8px; /* 相对定位祖先 */
right: -8px; /* 右上角外延 8px */
}
<style>
.card {
position: relative; /* ① 定位祖先 */
width: 160px;
height: 120px;
background: #f0f4ff;
}
.badge {
position: absolute; /* ② 脱离文档流,相对 .card 定位 */
top: -8px;
right: -8px;
background: #f5222d;
color: #fff;
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
</style>
<div class="card">
卡片内容
<span class="badge">99+</span> <!-- 红色角标钉在右上角 -->
</div>
4.1 absolute 的拉伸特性
当 absolute 元素同时设置了相对的两条边(如 top: 0; bottom: 0)且未指定高度时,元素会被拉伸到两端——这可以用来做"铺满"效果:
/* 遮罩铺满整个定位祖先 */
.overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, .4);
}
4.2 absolute + margin: auto 居中
如果 absolute 元素同时设 top/bottom 为 0,并配合 margin: auto,可以在定位祖先内垂直居中(无需知道尺寸):
.center-in-absolute {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 200px;
height: 120px;
}
/* 在定位祖先内水平垂直居中 */
4.3 absolute 的常见用途
角标/徽章、悬浮层、下拉菜单、模态框内部元素、图标覆盖层……凡是"叠在某个元素上面"的需求,基本都是 absolute。
五、position: fixed(固定定位)
fixed 让元素脱离文档流,相对**视口(viewport)**定位,不随页面滚动。
.back-to-top {
position: fixed;
right: 24px;
bottom: 24px;
/* 永远钉在视口右下角,滚动页面也不动 */
}
<style>
.back-to-top {
position: fixed;
right: 24px;
bottom: 24px;
width: 48px;
height: 48px;
border-radius: 50%;
background: #4f8cff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.2);
}
</style>
<div style="height: 2000px;">滚动这个页面,右下角的按钮始终固定</div>
<div class="back-to-top">↑</div>
重要陷阱:fixed 的相对基准是视口,但如果祖先元素设置了 transform、filter、perspective、will-change 等属性,该祖先会变成 fixed 的包含块——此时 fixed 就"退化成"相对该祖先的 absolute 定位,不再相对视口。
/* 陷阱示例:父元素加了 transform 后,fixed 子元素不再相对视口 */
.parent {
transform: translateZ(0); /* 常见于动画性能优化 */
}
.child-fixed {
position: fixed; /* 本意是钉在视口,实际相对 .parent 定位 */
}
这会让"返回顶部按钮 / 弹窗遮罩"在某些动画容器里失效。排查"fixed 为什么没生效"时,先检查有没有 transform 祖先。
常见用途:固定导航栏、返回顶部按钮、悬浮客服入口、全屏弹窗遮罩、移动端底部 tab 栏。
六、position: sticky(粘性定位)
sticky 是 relative 和 fixed 的结合体:在滚动到阈值之前,它像 relative 一样在文档流中;滚过阈值后,它像 fixed 一样"吸"住,但不会超出父容器边界。
.section-title {
position: sticky;
top: 0; /* 滚到距视口顶部 0px 时吸住 */
}
<style>
.list { }
.sticky-title {
position: sticky;
top: 0;
background: #4f8cff;
color: #fff;
padding: 10px 16px;
}
.item { padding: 12px; border-bottom: 1px solid #eee; }
</style>
<div>
<div class="sticky-title">分组 A</div>
<div class="item">内容 1</div><div class="item">内容 2</div>
<div class="sticky-title">分组 B</div>
<div class="item">内容 3</div><div class="item">内容 4</div>
</div>
<!-- 滚动时,"分组 A"吸在顶部;滚到"分组 B"时被顶上去 -->
6.1 sticky 生效的四个条件(缺一不可)
- 必须设置
top/bottom/left/right至少一个(通常用top),否则没有"吸住"的阈值。 - 父容器必须有足够的滚动空间:sticky 元素只能在父容器的高度范围内吸住,父容器太矮或刚好等高,就看不到吸顶效果。
- 滚动祖先不能是
overflow: hidden(auto/scroll可以):overflow: hidden会破坏 sticky。 - 不能是
display: inline(inline 元素对偏移不敏感,通常给display: block或flex项目)。
6.2 sticky 的常见用途
表格表头吸顶、分类列表标题吸顶、侧边栏跟随滚动、导航吸顶。
/* 表格表头吸顶 */
thead th {
position: sticky;
top: 0;
background: #f5f5f5;
z-index: 1;
}
七、z-index:层叠顺序
多个元素重叠时,z-index 决定谁在上。要点:
z-index只对定位元素(position 非 static)以及 Flex/Grid 项目生效。- 相同层叠上下文内,
z-index数值大的在上;数值相同按 DOM 顺序,后者在上。 z-index可以是负值(排到背景后面),但注意不要被祖先背景盖住。
.a { position: absolute; z-index: 2; }
.b { position: absolute; z-index: 1; }
/* a 在 b 之上 */
层叠上下文陷阱:设置了 transform、opacity < 1、filter、position + z-index 等属性会创建新的层叠上下文。子元素的 z-index 只能在父级层叠上下文内部比较,无法"越级"压过其他上下文——这就是"明明 z-index 设了 99999 还是被盖住"的常见原因。
/* a 是 z-index: 999 的子元素,但父容器层叠上下文更靠后,a 依然被压住 */
.parent-a { z-index: 1; position: relative; } /* 整个上下文靠后 */
.parent-b { z-index: 2; position: relative; } /* 整个上下文靠前 */
八、偏移属性 top/right/bottom/left 与 inset 简写
.element {
position: absolute;
top: 10px;
right: 20px;
bottom: 30px;
left: 40px;
}
- 四个方向可单独用,也可组合用(组合相对两侧会触发拉伸)。
- 值支持
px、%、em、calc()等。百分比相对包含块的对应尺寸计算。 inset是四者的简写,与margin/padding语法一致:
/* 等价写法 */
.overlay {
position: absolute;
inset: 0; /* top/right/bottom/left 全为 0 */
}
.edge {
position: absolute;
inset: 10px 20px; /* 上下 10px,左右 20px */
}
九、实战:6 个高频场景一次写清
9.1 角标 / 徽章
<style>
.card {
position: relative;
width: 120px;
height: 90px;
background: #f0f4ff;
border-radius: 8px;
}
.badge {
position: absolute;
top: -8px;
right: -8px;
background: #f5222d;
color: #fff;
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
}
</style>
<div class="card">
<span class="badge">3</span>
</div>
9.2 下拉菜单(absolute + hover)
<style>
.dropdown { position: relative; display: inline-block; }
.menu {
position: absolute;
top: 100%;
left: 0;
display: none;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
border-radius: 6px;
min-width: 120px;
padding: 6px 0;
}
.dropdown:hover .menu { display: block; }
.menu a { display: block; padding: 8px 16px; text-decoration: none; color: #333; }
.menu a:hover { background: #f5f5f5; }
</style>
<div class="dropdown">
<button>菜单 ▾</button>
<div class="menu">
<a href="#">选项一</a>
<a href="#">选项二</a>
<a href="#">选项三</a>
</div>
</div>
9.3 固定导航栏(fixed)
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: system-ui; }
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: #333;
color: #fff;
display: flex;
align-items: center;
padding: 0 24px;
}
.content { padding-top: 72px; } /* 给固定导航让位 */
</style>
<div class="navbar">固定导航</div>
<div class="content">
<p>滚动页面,导航栏始终固定在顶部。</p>
<p>(下拉填充高度模拟长页面……)</p>
</div>
9.4 返回顶部按钮(fixed)
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: system-ui; }
.long-page { height: 2000px; background: linear-gradient(#fff, #eef); padding: 24px; }
.back-top {
position: fixed;
right: 24px;
bottom: 24px;
width: 48px;
height: 48px;
border-radius: 50%;
background: #4f8cff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,.2);
}
</style>
<div class="long-page">往下滚动,右下角按钮不动</div>
<div class="back-top">↑</div>
9.5 表头吸顶(sticky)
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: system-ui; padding: 24px; }
.scroll-box { height: 300px; overflow: auto; border: 1px solid #ddd; }
table { border-collapse: collapse; width: 100%; }
th, td { padding: 10px; border: 1px solid #eee; }
thead th {
position: sticky;
top: 0;
background: #f5f5f5;
z-index: 1;
}
</style>
<div class="scroll-box">
<table>
<thead>
<tr><th>姓名</th><th>城市</th><th>部门</th></tr>
</thead>
<tbody>
<!-- 多行数据,向下滚动时表头吸在滚动框顶部 -->
<tr><td>张三</td><td>北京</td><td>前端</td></tr>
<tr><td>李四</td><td>上海</td><td>后端</td></tr>
<tr><td>王五</td><td>深圳</td><td>测试</td></tr>
</tbody>
</table>
</div>
9.6 全屏弹窗遮罩(fixed)
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: system-ui; }
.modal-mask {
position: fixed;
inset: 0; /* 铺满视口 */
background: rgba(0, 0, 0, .5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: #fff;
border-radius: 12px;
padding: 32px;
width: 320px;
box-shadow: 0 8px 24px rgba(0,0,0,.2);
}
</style>
<div class="modal-mask">
<div class="modal">
<h3>提示</h3>
<p style="margin: 12px 0;">这是一个全屏遮罩弹窗,页面可滚动但弹窗始终居中。</p>
<button onclick="this.parentElement.parentElement.style.display='none'">关闭</button>
</div>
</div>
十、五种定位总对比
| 取值 | 是否脱离文档流 | 定位基准 | 是否随滚动 | 典型用途 |
|---|---|---|---|---|
static | 否 | 文档流原位 | 是 | 默认状态 |
relative | 否(占位保留) | 自身原位置 | 是 | 微调位置、当定位祖先 |
absolute | 是 | 最近定位祖先(无则视口/html) | 是 | 角标、悬浮层、下拉菜单 |
fixed | 是 | 视口(transform 祖先除外) | 否 | 固定导航、返回顶部、弹窗遮罩 |
sticky | 否(滚动前占位) | 最近滚动祖先,受父容器约束 | 阈值内是,吸住后否 | 表头吸顶、分类吸顶、侧栏跟随 |
十一、高频踩坑与最佳实践
11.1 坑 1:absolute 的包含块不是父元素
忘记给父元素设 position: relative,absolute 子元素会一路向上找定位祖先,找不到就相对视口定位——结果跑到"莫名其妙"的位置。
/* 错:父元素没有定位,.badge 相对视口 */
.card { }
.badge { position: absolute; top: -8px; right: -8px; }
/* 对:父元素成为定位祖先 */
.card { position: relative; }
.badge { position: absolute; top: -8px; right: -8px; }
11.2 坑 2:fixed 被 transform 祖先"劫持"
祖先有 transform / filter / perspective 时,fixed 会相对该祖先而非视口。排查"fixed 失效"先看有没有这些祖先。
11.3 坑 3:sticky 不生效
逐个检查四条件:①没设 top/bottom/left/right;②父容器高度不够(没有滚动空间);③祖先 overflow: hidden;④元素是 inline。
11.4 坑 4:z-index 对非定位元素无效
z-index 只作用于定位元素(及 Flex/Grid 项目)。普通 static 元素写 z-index: 999 是无效的。
11.5 坑 5:父容器 overflow 截断
overflow: hidden 会截断 absolute 子元素(溢出部分不可见),也会破坏 sticky。需要"可滚动但可见"时用 overflow: auto。
11.6 坑 6:relative 不占位的误解
relative 虽然偏移,但原始占位保留,可能视觉上覆盖兄弟元素;需要"不占位"时用 absolute。
11.7 最佳实践小结
- "叠在某元素上"→ 父
relative+ 子absolute,固定搭配。 - "钉在视口不随滚动"→
fixed,但先确认没有 transform 祖先。 - "滚动后吸住"→
sticky,记得设top且保证父容器够高。 - 能不用
absolute布局就不用:常规排布优先文档流 + Flex/Grid,position用于"叠层"和"钉住"。 z-index只对定位元素生效,多层叠放时注意层叠上下文。- 固定导航/弹窗遮罩记得给主体内容让位(
padding-top/ 预留高度),避免内容被遮挡。
十二、总结
把这篇收敛成一张心智地图:
- static:默认,正常文档流。
- relative:相对自己偏移,不脱离文档流,最大的价值是当"定位祖先"。
- absolute:脱离文档流,相对最近的定位祖先——先找父级 relative,再谈 top/left。
- fixed:脱离文档流,相对视口,不随滚动;小心 transform 祖先。
- sticky:滚动到阈值前是 relative、之后像 fixed,但被父容器边界约束;记得设
top。 - z-index:只对定位元素生效,决定层叠谁在上,注意层叠上下文。
定位的底层逻辑就是一句话:先确定"相对谁"(包含块),再确定"怎么定"(是否脱离文档流),最后确定"偏移多少"(top/right/bottom/left)。把这三步想清楚,任何定位需求都不会再翻车。