Base UI 箭头消失之谜:floating-ui 只算一半偏移
给 Tooltip / Popover / HoverCard 写自定义箭头,样式写得没问题,hover 触发后 popup 出现了,箭头却凭空消失。调出 DevTools 才发现箭头方块就在 popup 内部躺着,被 -z-10 死死盖住,从来没探出去过。
现象
<BaseTooltip.Arrow> 内放一个旋转 45° 的方块当尖,四个朝向(top / bottom / left / right)只有一侧看起来"差不多对",其余三侧要么完全不见,要么箭头贴着 popup 内壁显示错位。增大 sideOffset 留出更多间隙反而把问题暴露得更明显——箭头和触发器之间有空白,就是没有桥。
根因
根因藏在 @base-ui-components/react 源码 esm/utils/useAnchorPositioning.js 里:
const arrowStyles = React.useMemo(() => ({
position: 'absolute',
top: middlewareData.arrow?.y, // left/right 侧才有值,top/bottom 侧为 undefined
left: middlewareData.arrow?.x, // top/bottom 侧才有值,left/right 侧为 undefined
}), [middlewareData.arrow]);Base UI 底层用的是 floating-ui 的 arrow middleware,该 middleware 只负责计算交叉轴居中:popup 在上方/下方时只给 left(水平居中),popup 在左侧/右侧时只给 top(垂直居中)。
把箭头从 popup 内部推出边缘的那一轴,floating-ui 压根不管。于是箭头方块停在 popup 内部的默认位置(top: 0, left: 0),被 -z-10 层叠彻底压住,视觉上"消失了"。
Base UI 官方示例的 CSS 里始终带着 [data-side=top] { bottom: -8px } 这类规则,正是在补这个缺失的偏移——只是文档里没有专门解释"为什么要补"。
arrowStyles 里永远只有 top 或 left 其中一个有值(另一个是 undefined),没有 bottom / right。Arrow 方块落在 popup 内部,被 -z-10 盖死。四个 side 只有某一侧"刚好对"也是误判——那只是恰好居中后视觉上没那么明显。
另一个小坑:data-side / data-align 是 Base UI 设在 Arrow 外层 <div> 上的,内层自定义 <span> 拿不到,Tailwind 的 data-[side=top]: 选择器必须写在外层。
正解
在 Arrow 的外层 className 上,按 data-side 补垂直偏移。偏移量 = 方块边长的一半(让方块中心落在 popup 边上,内半藏进去,外半露成尖)。8px 方块用 4px:
// 纯色对称菱形(Tooltip 常用,所有 side 旋转角相同)
<BaseTooltip.Arrow
className={cn(
"-z-10",
"data-[side=top]:bottom-[-4px]",
"data-[side=bottom]:top-[-4px]",
"data-[side=left]:right-[-4px]",
"data-[side=right]:left-[-4px]",
)}
>
<span className="block h-2 w-2 rotate-45 bg-foreground" />
</BaseTooltip.Arrow>带方向性 border 的箭头(card 型 popup,需要让特定角续上 popup 边线)稍复杂——方块用 border-b border-r 时 BR 角(右下)天然朝东南(罗盘 135°),要让该角朝向触发器需要按 side 旋转:
<BasePopover.Arrow
className={cn(
"-z-10",
"data-[side=top]:bottom-[-4px] data-[side=top]:rotate-45", // BR 角朝南
"data-[side=bottom]:top-[-4px] data-[side=bottom]:rotate-[225deg]", // BR 角朝北
"data-[side=left]:right-[-4px] data-[side=left]:rotate-[315deg]", // BR 角朝东
"data-[side=right]:left-[-4px] data-[side=right]:rotate-[135deg]", // BR 角朝西
)}
>
<span className="block h-2 w-2 border-b border-r border-border bg-surface" />
</BasePopover.Arrow>旋转量推导公式:目标方向(罗盘角)减去 BR 角初始方向(135°),即 rotate = target - 135°。
偏移写在 Arrow 外层 className;两轴互不干扰(floating-ui 设的轴 vs 自补的轴正交);Base UI 视口碰撞翻转(flip)后 data-side 跟着变,偏移+旋转自动正确,无需额外处理。
一句话外卖
floating-ui 的 arrow middleware 只算交叉轴居中,垂直于边那一轴必须自己按
data-side补——这是 Base UI 所有 overlay 箭头的通用规律,Tooltip / Popover / HoverCard / Menu / Select 全部适用。