在前端开发中,我们常遇到这样的问题:
为什么我的 tooltip、弹窗、下拉菜单明明是
position: fixed,却还是被父元素裁剪、遮挡?
很多人以为是 overflow: hidden 或 z-index 的问题,但实际上------根本原因在于 "包含块 (containing block)" 的创建机制。
1. overflow 本身不会影响 fixed
单独的 overflow: hidden 并不会让 fixed 元素失效:
css
<div style={{ overflow: 'hidden' }}>
<div style={{ position: 'fixed', top: 0 }}>我是 fixed</div>
</div>
上例中,fixed 仍然相对 视口 (viewport) 定位,不会被裁剪。
2. 谁在"偷走"你的 fixed?
当某个祖先元素具有以下任意属性时,它会创建一个新的 包含块 (containing block) ,从而让 fixed 元素失去对 viewport 的参照:
| 属性 | 示例 |
|---|---|
| transform | transform: translate(0, 0) |
| filter | filter: blur(0) |
| perspective | perspective: 1000px |
| contain | contain: layout/paint |
| will-change | will-change: transform |
| backdrop-filter | backdrop-filter: blur(10px) |
一旦出现这些属性,position: fixed 的元素将不再相对窗口定位,而是相对该祖先元素定位。
3. 被裁剪的真相
如果上述祖先元素同时还设置了 overflow: hidden 或 overflow: auto,
则 fixed 元素的可视区域也会被父元素裁剪掉:
less
<div style={{
transform: 'translateZ(0)', // 关键:创建包含块
overflow: 'hidden',
border: '1px solid red',
height: 100,
width: 100,
}}>
<div style={{
position: 'fixed',
top: 0,
left: -20,
background: 'black',
color: 'white'
}}>
我被裁剪了
</div>
</div>

4. ReactDOM.createPortal 的作用
ReactDOM.createPortal 会把组件直接渲染到 document.body(或指定容器):
javascript
ReactDOM.createPortal(
<Tooltip />,
document.body
)
这样做可以完全绕过那些"有问题的父容器",
确保你的 tooltip、弹窗永远相对 viewport 定位,不受裁剪或 transform 干扰。
5. Tooltip 对比示例
下面这段代码展示了同样的 tooltip,在 非 portal 与 portal 模式下的差异。
less
import React from 'react';
import ReactDOM from 'react-dom';
function NonPortalTooltip() {
return (
<div style={{
position: 'relative',
width: 200,
height: 120,
overflow: 'hidden',
transform: 'translateZ(0)', // 创建包含块
border: '1px solid #aaa',
marginBottom: 20
}}>
<div style={{ padding: 8 }}>父容器(overflow + transform)</div>
<div style={{
position: 'fixed',
top: 80, left: 150,
background: 'black',
color: 'white',
padding: '4px 8px'
}}>
非 Portal Tooltip(会被裁剪)
</div>
</div>
);
}
function PortalTooltip() {
return (
<div style={{
position: 'relative',
width: 200,
height: 120,
overflow: 'hidden',
transform: 'translateZ(0)',
border: '1px solid #aaa'
}}>
<div style={{ padding: 8 }}>父容器(overflow + transform)</div>
{ReactDOM.createPortal(
<div style={{
position: 'fixed',
top: 80, left: 150,
background: 'green',
color: 'white',
padding: '4px 8px'
}}>
Portal Tooltip(不会被裁剪)
</div>,
document.body
)}
</div>
);
}
export default function App() {
return (
<>
<NonPortalTooltip />
<PortalTooltip />
</>
);
}
运行后你会看到:
- 黑色 tooltip(非 portal)会被父容器裁剪;

- 绿色 tooltip(通过 portal 渲染)则始终完整可见。


✅ 一句话总结
position: fixed相对视口定位的前提是:祖先元素没有创建新的包含块 。而
ReactDOM.createPortal的意义,就是让元素彻底脱离这些祖先的影响。