CSS 定位机制详解

中等 🟡CSS 布局
6 个标签
预计阅读时间:34 分钟
CSS定位positionz-indexsticky层叠上下文

CSS 定位机制详解

CSS 定位(position)用来精确控制元素在页面上的位置,是布局体系里除 Flexbox、Grid 之外不可或缺的一环。它决定了一个元素究竟「随文档流走」,还是「脱离文档流独立摆放」。理解定位机制,是实现弹窗、悬浮按钮、吸顶导航、下拉菜单等常见交互的前提。

一个类比先理解它

把网页想象成一面公告墙。默认情况下,所有纸条(元素)按顺序一张接一张贴上去,互不重叠,这就是正常文档流(static)。而 position 属性就像给纸条换了几种「粘贴方式」:relative 是「在原位微调一点点,但原来的位置仍然留着」;absolute 是「从墙上揭下来,用图钉钉到指定坐标,原位置的空缺被后面的纸条填补」;fixed 是「钉在你眼前的透明玻璃罩上,无论墙怎么滚动它都不动」;sticky 则是「先跟着墙走,滚到某条线就吸住不动」。

为什么定位机制重要

现代交互中大量元素需要「浮」在内容之上或「固定」在某处:模态框、Toast 提示、悬浮客服、吸顶导航、图片角标、下拉面板……这些都离不开定位。用错定位类型会引发一连串问题:内容被遮挡、元素错位、z-index 失控、滚动时抖动。掌握五种定位及其对文档流的影响,能从根源上避免这些坑。

定位类型

static(默认值):

元素处于正常文档流,按 HTML 顺序自然排列。
top / right / bottom / left / z-index 全部无效。
想让这些偏移属性生效,必须改成其他定位类型。

relative(相对定位):

相对于元素自己在文档流中的原始位置做偏移。
不脱离文档流:原位置的空间被保留,其他元素不会来填补。
最重要的隐藏用途:作为 absolute 子元素的「定位参照物」(建立包含块)。

absolute(绝对定位):

相对于最近的、非 static 定位祖先元素定位。
脱离文档流:原位置的空间被释放,后续元素上移填补。
若找不到定位祖先,则相对于初始包含块(通常是视口/根元素)。

fixed(固定定位):

相对于视口定位,滚动页面时位置不变。
脱离文档流。
注意:祖先若设置了 transform、filter、perspective 等属性,会「劫持」fixed 的参照物,使其相对该祖先而非视口——这是最隐蔽的坑之一。

sticky(粘性定位):

相对定位与固定定位的混合体。
在滚动到达阈值前表现为 relative,到达后「吸附」表现为 fixed。
必须指定 top / bottom / left / right 中至少一个作为吸附阈值。
吸附范围受限于其父容器:一旦父容器滚出视口,sticky 元素也随之离开。

五种定位速查

| 类型 | 参照物 | 是否脱离文档流 | 偏移属性生效 | 典型场景 |

| --- | --- | --- | --- | --- |

| static | 无 | 否 | 否 | 默认排版 |

| relative | 自身原位置 | 否(保留空间) | 是 | 微调、作定位父级 |

| absolute | 最近定位祖先 | 是 | 是 | 弹层、角标、下拉 |

| fixed | 视口 | 是 | 是 | 吸顶导航、返回顶部 |

| sticky | 滚动容器 | 否(阈值前) | 是 | 吸顶表头、章节标题 |

层叠上下文与 z-index

z-index 控制元素在 Z 轴(垂直屏幕方向)的堆叠顺序,但它只在定位元素(非 static)或特定场景(Flex/Grid 子项、opacity < 1 等)下生效,且受「层叠上下文(stacking context)」约束:

一个元素设置 position 非 static 且 z-index 非 auto,会创建新的层叠上下文。
子元素的 z-index 无论多大,都只在父层叠上下文内部比较,无法「越级」压过父级的兄弟。
这解释了「为什么我把 z-index 设成 9999 还是被盖住」——因为父级层叠上下文的层级更低。

创建层叠上下文的常见触发条件:position + z-index、opacity 小于 1、transform、filter、will-change、isolation: isolate 等。想主动隔离某个模块可用 isolation: isolate。

代码示例

模态框(遮罩 + 内容居中 + 过渡)

cssCode
/* 遮罩层:fixed 铺满视口 */
.modal-overlay {
  position: fixed;
  inset: 0; /* 等价于 top:0; right:0; bottom:0; left:0; */
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}

.modal-overlay.active {
  opacity: 1;
  visibility: visible;
}

/* 内容:relative 便于内部绝对定位(如关闭按钮) */
.modal {
  position: relative;
  width: 90%;
  max-width: 600px;
  max-height: 90vh;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.2);
  overflow-y: auto;
  transform: translateY(-20px);
  transition: transform 0.3s;
}

.modal-overlay.active .modal {
  transform: translateY(0);
}

.modal-header {
  padding: 20px;
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.modal-close {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  color: #999;
  transition: color 0.3s;
}
.modal-close:hover { color: #333; }

.modal-body { padding: 20px; }
.modal-footer {
  padding: 20px;
  border-top: 1px solid #eee;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

固定导航栏与返回顶部按钮

cssCode
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 60px;
  background-color: #333;
  color: white;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
}

/* 关键:为 body 留出 header 高度,避免内容被遮挡 */
body { padding-top: 60px; }

.back-to-top {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 50px;
  height: 50px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.back-to-top.visible { opacity: 1; visibility: visible; }

绝对定位角标(图片右上角红点/徽章)

cssCode
.avatar {
  position: relative; /* 建立定位上下文 */
  display: inline-block;
}

.avatar__badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: #f5222d;
  color: #fff;
  font-size: 12px;
  line-height: 18px;
  text-align: center;
}

sticky 吸顶(表格表头 / 章节标题)

cssCode
.section-title {
  position: sticky;
  top: 0;          /* 滚动到顶部时吸附 */
  background: #fff;
  z-index: 10;
  padding: 8px 0;
}

/* 表格表头吸顶 */
thead th {
  position: sticky;
  top: 0;
  background: #fafafa;
}

绝对定位实现居中(不依赖 Flex 时)

cssCode
.centered {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%); /* 用自身尺寸的一半回移,精确居中 */
}

下拉菜单(相对父级定位)

cssCode
.dropdown {
  position: relative;
}

.dropdown__panel {
  position: absolute;
  top: 100%;      /* 紧贴触发按钮底部 */
  left: 0;
  min-width: 160px;
  background: #fff;
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  border-radius: 6px;
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  transition: opacity 0.2s, transform 0.2s;
}

.dropdown:hover .dropdown__panel {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

真实案例:吸顶筛选栏被内容盖住的排查

某商品列表页要求筛选栏滚动到顶部后吸附。开发者写了 position: sticky; top: 0,本地正常,但线上滚动时筛选栏被下方卡片「盖住」。逐步排查:

1.检查 z-index:sticky 元素未设 z-index,而卡片带阴影创建了层叠上下文,导致后者覆盖前者。给筛选栏加 z-index: 20 后仍有个别页面失效。
2.检查父容器:失效页面的某个祖先设置了 overflow: hidden,导致 sticky 的滚动容器变成该祖先,一滚出去就失去吸附。改用 overflow: clip 或移除该属性后恢复。
3.检查 transform:另一处 fixed 的悬浮按钮在某设备上「跟着内容滚动」,最终定位到祖先有 transform: translateZ(0) 硬件加速,劫持了 fixed 的参照物。移除后修复。

这三点正是定位相关线上问题的三大高频根因:z-index 层叠上下文、overflow 截断 sticky、transform 劫持 fixed。

更多实战场景

Tooltip 气泡(带小三角)

cssCode
.tooltip {
  position: relative;
  display: inline-block;
}

.tooltip__bubble {
  position: absolute;
  bottom: calc(100% + 8px); /* 位于触发元素上方,留 8px 间隙 */
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 10px;
  background: #333;
  color: #fff;
  border-radius: 4px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}

/* 小三角:用绝对定位的伪元素 */
.tooltip__bubble::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: #333;
}

.tooltip:hover .tooltip__bubble { opacity: 1; }

全屏 Loading 遮罩

cssCode
.loading-mask {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.8);
  z-index: 9000;
}

.loading-mask__spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #eee;
  border-top-color: #007bff;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

卡片右上角「NEW」角标

cssCode
.card {
  position: relative;
  overflow: hidden; /* 裁掉超出卡片的角标部分 */
}

.card__ribbon {
  position: absolute;
  top: 12px;
  right: -30px;
  transform: rotate(45deg);
  width: 120px;
  text-align: center;
  background: #f5222d;
  color: #fff;
  font-size: 12px;
  line-height: 20px;
}

表格首列 + 表头同时冻结

cssCode
.table-wrap {
  overflow: auto;
  max-height: 400px;
}

/* 表头吸顶 */
thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #fafafa;
}

/* 首列冻结 */
tbody td:first-child,
thead th:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: #fff;
}

/* 左上角交叉单元需更高层级,避免被覆盖 */
thead th:first-child { z-index: 3; }

悬浮操作按钮组(FAB)

cssCode
.fab {
  position: fixed;
  right: 24px;
  bottom: 24px;
  display: flex;
  flex-direction: column-reverse;
  gap: 12px;
  z-index: 1500;
}

relative vs absolute vs fixed:偏移基准对比

同样写 top: 20px; left: 20px,三种定位的效果截然不同:

| 定位 | top/left 参照 | 是否随滚动移动 | 原位置是否保留 |

| --- | --- | --- | --- |

| relative | 元素自身原始位置 | 是(跟随文档流) | 是 |

| absolute | 最近定位祖先的内边距盒 | 跟随祖先 | 否 |

| fixed | 视口 | 否(钉死屏幕) | 否 |

理解这张表能避免 80% 的「元素跑错位置」问题:先确认参照物是谁,再算偏移。

包含块(Containing Block)原理

绝对定位元素的位置由它的「包含块」决定。包含块的确定规则:

position 为 absolute:包含块是最近的 position 不为 static 的祖先的内边距边界。
position 为 fixed:包含块通常是视口;但若祖先设置了 transform、filter、perspective、will-change、contain: layout/paint 等,会成为新的包含块,「劫持」fixed。
百分比尺寸(如 width: 50%)也是相对包含块计算的。

这解释了为何「给父级加 position: relative」是绝对定位的必备前置动作——它把父级变成了子元素的包含块。

z-index 分层建议(团队规范示例)

统一约定层级区间能显著减少「z-index 军备竞赛」:

| 层级区间 | 用途 |

| --- | --- |

| 0 - 9 | 普通内容微调 |

| 10 - 99 | 吸顶栏、粘性表头 |

| 100 - 999 | 下拉、气泡、tooltip |

| 1000 - 1999 | 固定导航、悬浮按钮 |

| 2000 - 8999 | 模态框、抽屉 |

| 9000+ | Toast、全局 loading |

常见坑与排查

| 现象 | 常见原因 | 解决办法 |

| --- | --- | --- |

| absolute 元素跑到页面左上角 | 没有非 static 定位祖先 | 给父级加 position: relative |

| z-index 设很大仍被盖住 | 受父层叠上下文限制 | 提升父级层级或用 isolation |

| fixed 元素跟着内容滚动 | 祖先有 transform/filter | 移除该属性或调整结构 |

| sticky 完全不生效 | 未设 top/bottom 或父级 overflow:hidden | 补阈值、检查父级 overflow |

| 固定导航遮住正文 | 未给 body 留空间 | body 加 padding-top |

| top/left 无效 | 元素仍是 static | 改为 relative/absolute/fixed |

最佳实践

常规布局优先用 Flexbox / Grid,定位只用于「浮层、固定、微调」等特殊场景。
absolute 之前先给合适的父级加 position: relative,明确定位参照。
建立并遵守全站 z-index 分层规范,避免随手写 9999。
用 inset: 0 简写替代四条 top/right/bottom/left。
fixed 元素警惕祖先的 transform/filter 劫持。
sticky 元素记得设阈值,并检查父链上的 overflow。
为固定/吸顶元素预留内容空间(padding 或占位)。
测试不同屏幕尺寸与滚动场景下的表现。

深入:sticky 定位的完整触发条件

sticky 是五种定位里最「挑食」的一个,很多人写了 position: sticky 却不生效,根源往往在下面这几条隐性条件:

1.必须指定至少一个阈值:top / bottom / left / right 之一,否则沙滩上没有「吸附线」,元素永远表现为 relative。
2.父容器必须有滚动空间:sticky 的吸附范围严格限定在直接父容器内,父容器一旦滚出视口,sticky 也随之离开。
3.任何祖先都不能有 overflow: hidden / auto / scroll:一旦某个祖先设了非 visible 的 overflow,它就变成 sticky 的滚动容器,常导致「完全不吸附」或「吸附范围异常」。
4.父容器高度要大于 sticky 元素:父容器和元素一样高时,没有可滚动的余量,吸附无从谈起。
cssCode
/* 正确的 sticky 侧边目录 */
.layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 24px;
  /* 注意:这里不要给外层加 overflow: hidden */
}

.toc {
  position: sticky;
  top: 80px;          /* 距顶 80px 处吸附(给固定头留空间) */
  align-self: start;  /* Grid 中关键:不拉伸才能真正 sticky */
  max-height: calc(100vh - 100px);
  overflow-y: auto;
}

在 Grid / Flex 布局里,sticky 子项还有一个额外坑:align-items 默认 stretch 会把子项拉满整列高度,等于「没有滚动余量」。给 sticky 项加 align-self: start 是这类场景的必备一步。

深入:层叠上下文的完整创建条件

「z-index 设了 9999 还被盖住」几乎都源于层叠上下文(stacking context)理解不到位。一个元素满足以下任一条件,就会创建新的层叠上下文,其内部所有 z-index 从此只在这个「小世界」里比较:

| 触发条件 | 说明 |

| --- | --- |

| 根元素 html | 天然的顶层层叠上下文 |

| position + z-index 非 auto | relative/absolute/fixed/sticky 且设了 z-index |

| position: fixed / sticky | 无论 z-index 是否设置 |

| opacity 小于 1 | 半透明元素自成一层 |

| transform / filter / perspective 非 none | 常见「意外」触发点 |

| will-change 指定了会创建上下文的属性 | 如 will-change: transform |

| isolation: isolate | 专门用来主动隔离 |

| mix-blend-mode 非 normal | 混合模式 |

| contain: layout / paint / strict | 包含性优化 |

关键规则:子元素的 z-index 再大,也无法越过父层叠上下文去和父级的兄弟比较。

cssCode
/* 问题重现:panel 的 z-index:9999 仍被 sibling 盖住 */
.card {
  position: relative;
  transform: translateZ(0); /* ← 罪魁:创建了层叠上下文 */
}
.card .panel { position: absolute; z-index: 9999; } /* 被困在 card 内部 */
.sibling { position: relative; z-index: 1; } /* 却盖住了 panel */

/* 解法一:去掉 card 上多余的 transform */
/* 解法二:把 panel 用 portal 渲染到 body 顶层 */
/* 解法三:给需要压过的容器整体提层级 */
.card { z-index: 10; }

现代组件库(如 Radix、MUI)之所以普遍用「Portal 把浮层渲染到 body 末尾」,正是为了彻底绕开层叠上下文的嵌套困局——浮层直接挂在根上下文下,天然能盖住页面任何内容。

深入:inset 与逻辑定位属性

inset 是 top / right / bottom / left 的简写,语法与 margin 一致:

cssCode
.fill { position: absolute; inset: 0; }              /* 四边贴满 */
.pad  { position: absolute; inset: 10px; }           /* 四边各留 10px */
.rect { position: absolute; inset: 10px 20px; }      /* 上下 10、左右 20 */
.mix  { position: absolute; inset: 10px 20px 30px 40px; } /* 上右下左 */

配合逻辑属性还能做到国际化友好的定位(RTL 下自动镜像):

cssCode
/* 物理属性:RTL 语言下位置错误 */
.badge-physical { position: absolute; top: 0; right: 0; }

/* 逻辑属性:inset-block 管块向、inset-inline 管行向,随书写方向自适应 */
.badge-logical {
  position: absolute;
  inset-block-start: 0;   /* 相当于 top */
  inset-inline-end: 0;    /* LTR 下是 right,RTL 下自动变 left */
}

真实案例:抽屉(Drawer)+ 遮罩的层级与滚动锁

侧滑抽屉是定位、层叠、滚动锁三者的综合考题。要点:遮罩 fixed 铺满并高层级、抽屉 fixed 贴边并用 transform 滑入、打开时锁住 body 滚动防止「背景穿透滚动」。

cssCode
.drawer-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 2000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.drawer-mask.open { opacity: 1; visibility: visible; }

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(360px, 85vw);
  background: #fff;
  z-index: 2001;             /* 高于遮罩 */
  transform: translateX(100%); /* 初始滑出屏幕右侧 */
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: -8px 0 24px rgba(0, 0, 0, 0.12);
}
.drawer.open { transform: translateX(0); } /* 滑入 */

/* 打开时给 body 加此类,锁住背景滚动 */
.body-lock { overflow: hidden; }

用 transform 而非改 right 做滑动动画,是因为 transform 只走合成层、不触发重排,能稳定跑满 60fps。抽屉宽度用 min(360px, 85vw) 保证在小屏上也留出可见的遮罩区,用户点遮罩即可关闭。

真实案例:固定头 + 锚点跳转的偏移修正

带固定头(fixed header)的页面,点击锚点跳转时目标标题会被头部遮挡一截,这是极其高频的体验缺陷。纯 CSS 一行即可修复:

cssCode
/* 固定头高 64px */
.site-header { position: fixed; top: 0; inset-inline: 0; height: 64px; }

/* 关键:给所有锚点目标预留滚动偏移,跳转时自动下移 64px + 余量 */
:target,
[id] {
  scroll-margin-top: 80px;
}

在有 scroll-margin-top 之前,人们要靠「透明占位伪元素 + 负 margin」这种 hack,既啰嗦又易错。scroll-margin-top 是专门为「固定头锚点跳转」设计的属性,一行解决。

absolute 居中的三种写法对比

不依赖 Flex/Grid 时,绝对定位居中有几种写法,各有适用场景:

cssCode
/* 写法一:transform 回移(元素尺寸未知时首选) */
.center-transform {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/* 写法二:inset:0 + margin:auto(元素需有明确尺寸) */
.center-margin {
  position: absolute;
  inset: 0;
  width: 200px;
  height: 120px;
  margin: auto; /* 四边 auto 均分,实现居中 */
}

/* 写法三:现代 anchor 定位(新特性,兼容性有限) */
.center-modern {
  position: absolute;
  inset: 0;
  place-self: center; /* 部分浏览器支持绝对定位元素的自对齐 */
}

| 写法 | 是否需知尺寸 | 亚像素模糊风险 | 适用 |

| --- | --- | --- | --- |

| transform 回移 | 否 | 有(半像素时文字发虚) | 尺寸不定的弹层 |

| inset + margin auto | 是 | 无 | 尺寸固定的图片/卡片 |

| place-self | 否 | 无 | 现代浏览器项目 |

transform: translate(-50%, -50%) 在元素尺寸为奇数像素时可能落在半像素上导致文字发虚,对清晰度敏感时优先用 inset + margin auto。

定位与滚动性能

fixed 与 sticky 元素在滚动时需要浏览器持续重算位置,用不当会拖累滚动帧率:

| 关注点 | 说明 | 建议 |

| --- | --- | --- |

| 大量 sticky 元素 | 每个都参与滚动计算 | 控制数量,避免每行都 sticky |

| fixed 元素内做重排动画 | 滚动时反复重排 | 动画只用 transform/opacity |

| 半透明 fixed 遮罩 | 大面积重绘 | 必要时用 will-change 提层,用完移除 |

| sticky + 复杂阴影 | 吸附瞬间重绘范围大 | 简化阴影或改用边框 |

深入:CSS Anchor Positioning(锚点定位新特性)

传统上,把下拉菜单、tooltip 精确「贴」在触发元素旁边、并在空间不足时自动翻转方向,都得靠 JS 库(如 Popper.js / Floating UI)实时测量。CSS Anchor Positioning 让这类需求首次可以纯 CSS 实现。

cssCode
/* 声明触发元素为锚点 */
.trigger {
  anchor-name: --my-anchor;
}

/* 浮层绑定到该锚点,定位在其下方 */
.popover {
  position: absolute;
  position-anchor: --my-anchor;
  /* 顶边贴锚点底边,左边对齐锚点左边 */
  top: anchor(bottom);
  left: anchor(left);
  margin-top: 8px;
}

/* 空间不足时自动翻转到上方(position-try 回退列表) */
.popover {
  position-try-fallbacks: flip-block;
}

Anchor Positioning 自 Chrome 125 起可用,Safari / Firefox 仍在跟进。生产环境仍建议用 JS 库兜底,但了解这一方向能帮助判断技术演进——未来大量「浮层跟随定位」逻辑会从 JS 回归 CSS。

真实案例:卡片 hover 浮起 + 悬浮操作层

商品卡片常见的交互:默认平铺,鼠标悬停时整卡轻微浮起,并从底部滑出一层「加购 / 收藏」操作条。这里 absolute 负责操作层贴底,relative 父级建立参照,transform 负责浮起与滑入。

cssCode
.product {
  position: relative;             /* 操作层的定位参照 */
  overflow: hidden;              /* 裁掉初始时藏在下方的操作条 */
  border-radius: 12px;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.product:hover {
  transform: translateY(-6px);   /* 整卡浮起 */
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
}

.product__actions {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: 12px;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
  transform: translateY(100%);   /* 初始藏在卡片下缘之外 */
  transition: transform 0.25s ease;
}

.product:hover .product__actions {
  transform: translateY(0);      /* 悬停时滑入 */
}

要点:操作条用 transform 平移进出而非改 bottom,保证只走合成层;父级 overflow: hidden 既裁掉初始藏起的操作条,也顺便把图片圆角裁齐。

五种定位的选择决策树

面对一个定位需求,按下面顺序自问,就能快速选对类型:

1.要不要脱离文档流、影响其他元素排布?不需要 → 优先 static / relative。
2.只是想微调自身位置、或给子元素当参照?→ relative。
3.要浮在内容之上、贴着某个父级摆放?→ 父级 relative + 子级 absolute。
4.要钉死在屏幕某处、不随滚动移动?→ fixed(并警惕祖先 transform 劫持)。
5.要「先跟随、滚到某处再吸住」?→ sticky(记得设阈值、检查父链 overflow)。
cssCode
/* 决策示例:一个既要吸顶、内部又要放浮层的工具栏 */
.toolbar {
  position: sticky;   /* 需求 5:滚动吸顶 */
  top: 0;
  z-index: 30;
  background: #fff;
}
.toolbar .menu-host {
  position: relative; /* 需求 3:给下拉菜单当参照 */
}
.toolbar .menu {
  position: absolute; /* 浮层贴在触发器下方 */
  top: 100%;
  left: 0;
}

总结

| 维度 | 关键点 |

| --- | --- |

| 五种类型 | static / relative / absolute / fixed / sticky |

| 是否脱流 | absolute、fixed 脱离;relative、sticky 保留空间 |

| 参照物 | relative 看自身、absolute 看定位祖先、fixed 看视口、sticky 看滚动容器 |

| z-index | 仅定位元素生效,受层叠上下文约束 |

| 三大坑 | 无定位祖先、overflow 截断 sticky、transform 劫持 fixed |

| 常用组合 | relative 父 + absolute 子构建浮层 |

一句话记忆:先问「这个元素要不要脱离文档流、以谁为参照」,再选定位类型;浮层记得给父级 relative,层级冲突先想层叠上下文而非无脑加 z-index。