Base UI 导航菜单面板塌缩成 2×2px 的根因:Content 不能脱离文档流
给导航栏做 mega 菜单时,我用了 Base UI 的 NavigationMenu。面板动画看起来很顺,
然而上线后用户反映菜单「点了没反应」——仔细检查才发现触发器的 chevron 确实旋转了(
data-popup-open 上去了),DOM 里内容也在,就是肉眼完全看不见。
现象
打开 DevTools,选中 Popup 元素:
// 控制台直接量一下
const popup = [...document.querySelectorAll('*')]
.find(e => (e.style.width || '').includes('--popup-width'));
console.log(JSON.stringify(popup.getBoundingClientRect()));
// 输出:{"x":120,"y":64,"width":2,"height":2,"top":64,"right":122,"bottom":66,"left":120}宽 2、高 2。再看计算样式:--popup-width: auto、--popup-height: auto——
这两个 CSS 变量从没写入像素值,Popup 的 width: var(--popup-width) 于是退化为
width: auto,内容全部溢出到 overflow: hidden 区域之外,面板肉眼消失。
构建和单元测试全部绿,因为它们只断言 DOM 存在或类名,从不量 bounding box。
根因
Base UI 的 Popup 尺寸变形(size morph)依赖一次主动测量:
- Trigger 被激活 → 把
--popup-width/height先置为auto - 调用
getCssDimensions(popupElement)读取 Popup 的自然尺寸 - 把读到的 px 值写回
--popup-width/height,再触发 CSS transition 做大小动画 - 动画结束 → 重置回
auto(让 Popup 跟内容尺寸)
第 2 步的测量结果完全取决于 Popup 此刻能撑多大——而这正由其子元素 Content
贡献。如果 Content 被设成 position: absolute,它就脱离了文档流,不参与
父元素的高度计算,Popup 自然尺寸归零,测量结果 ≈ 0,于是 --popup-width/height
被写入接近 0 的值,面板塌缩。
直觉上「面板是浮层,多个 Content 切换时应该叠加,自然要用 absolute」——但 Base UI
的 size morph 机制要求当前激活的 Content 必须留在文档流中,以便 Popup 能
量到真实尺寸。一旦把它设成 absolute,Popup 测出宽高为 0,CSS 变量写入 0 px,
overflow: hidden 把面板彻底裁掉,症状就是「内容挂了但面板不可见」。
正解
只把正在退出(exiting)的 Content 设为 absolute,当前激活的那个保持 in-flow:
// ❌ 两个状态都 absolute → 当前 Content 也脱流 → Popup 量到 0
<NavigationMenu.Content className="absolute left-0 top-0 w-full p-6" />
// ✅ 当前 Content 在流内;仅 data-ending-style 时脱流
<NavigationMenu.Content
style={{ transition: `opacity 200ms ease, transform 200ms ease` }}
className={cn(
// 留在流内,让 Popup 自然撑开
"w-max max-w-[calc(100vw-2rem)] p-6",
// 仅退出时脱流,不影响当前内容的尺寸贡献
"data-[ending-style]:absolute data-[ending-style]:left-0 data-[ending-style]:top-0",
// 进入/退出淡化
"data-[starting-style]:opacity-0 data-[ending-style]:opacity-0",
)}
/>同时确保:
- Popup 加
overflow-hidden(裁剪 size morph 过渡期的溢出),并绑定width: var(--popup-width); height: var(--popup-height)+ width/height transition - Viewport 设
position: relative(为退出时的 absolute Content 提供定位上下文) - 位移动画(left/right/top/bottom)放在 Positioner,不要放在 Popup
修复后再量:
JSON.stringify(popup.getBoundingClientRect());
// {"x":120,"y":64,"width":482,"height":146,...} ✅把 position: absolute 限制在 data-[ending-style],进入/激活状态的 Content
始终在文档流中。这样 getCssDimensions 每次都能量到真实内容尺寸,size morph
的 CSS 变量得到正确像素值,面板正常显示。
排查备忘
这类坑单元测试和 SSG 构建都不会暴露,因为它们不量几何。遇到「内容挂载但面板
不可见」时,先在 DevTools 或 CDP 里量 Popup 的 getBoundingClientRect(),
如果宽高接近 0 并且 --popup-width/height 还是 auto,基本可以断定是
getCssDimensions 量到了 0——往 Content 的 position 检查。
一句话外卖
Base UI 通过测量 Popup 的自然尺寸驱动 size morph,绝不能让激活态的 Content 脱离文档流;只把退出态设为
absolute,入场态留在流内。