Chakra Tooltip 邊界顯示時 scrollbar 閃爍的修法
問題
Chakra Tooltip 出現的瞬間,網頁會多出水平 + 垂直 scrollbar 各一條,下一個 frame 又消失。Tooltip 收起來之後一切正常。
肉眼看就是個閃爍,但影響很惱人:
- 內容區域因為 scrollbar 突然出現而被擠掉幾 px,layout shift
- 在 sticky / fixed 元素旁邊看起來特別明顯
- 視覺上像是 page 在抽搐
核心原因
Chakra Tooltip 底層用 @popperjs/core 定位,預設的 strategy 是 "absolute",而且 Chakra 沒有把這個選項透過 props 暴露出來。
absolute strategy 的行為:
- popper element 用
position: absolute定位 - layout box 的參考座標是離它最近的
position非static的祖先——通常會冒到<body> - popper element 雖然視覺上會被定位到正確位置,但它的盒子是計入
<html>的 scrollable area 的
當 tooltip 出現在接近 viewport 邊界時:
- 一個 frame:popper element 先以
absolute被插入並定位,盒子撐出<html>的 scroll area - 瀏覽器看到 overflow → 跑出 scrollbar
- 下一個 frame:popper 的 placement 計算修正完成,盒子收回 viewport 內
- overflow 消失 → scrollbar 也消失
整個過程在一個 render frame 內完成,但 scrollbar 還是會閃一下。
解法
唯一暴露出來的缺口是 Tooltip 接受 popper 的 modifiers prop。寫一個 modifier 把 strategy 從 'absolute' 改成 'fixed':
import type { Modifier } from '@popperjs/core';
const fixedStrategyModifier: Partial<Modifier<'fixedStrategy', object>> = {
name: 'fixedStrategy',
enabled: true,
phase: 'beforeRead',
fn: ({ state }) => {
state.options.strategy = 'fixed';
state.strategy = 'fixed';
},
effect: ({ state }) => {
state.options.strategy = 'fixed';
state.strategy = 'fixed';
state.elements.popper.style.position = 'fixed';
},
};
const tooltipModifiers = [fixedStrategyModifier];用法:
<Tooltip
placement="bottom"
hasArrow
label={t('tooltipBulkSetAllocation')}
shouldWrapChildren
isDisabled={isLocked}
modifiers={tooltipModifiers}
>
...
</Tooltip>為什麼三個地方都要改
| 改動位置 | 何時跑 | 為什麼必要 |
|---|---|---|
effect 裡 state.options.strategy | popper instance 初始化時跑一次 | 設定 popper 的「設定」,影響後續所有 update |
effect 裡 state.elements.popper.style.position | 初始化時直接寫 DOM | 第一次 render 就用 position: fixed,避免「先 absolute 一個 frame 再修正」 |
fn 裡 state.options.strategy / state.strategy | 每次 update(scroll、resize、placement 變化)都跑 | popper 內部某些路徑會把 strategy 重設回預設,所以 update 時要再壓一次 |
少寫任一段都會在特定時序下漏網——特別是 effect 裡如果只改 options 沒改 inline style,第一次 mount 還是會閃。
fixed 為什麼能解決
position: fixed 的 layout box 是對 viewport 而不是 <body>,根本不計入 <html> 的 scrollable area。所以不管 popper 一開始算錯位置在哪,都不會撐大 <html> overflow,scrollbar 不會被觸發。
Trade-offs / 注意事項
- Fixed 對 scroll 容器內的 tooltip 行為不同:如果 Tooltip 是在一個有
overflow: auto/scroll的容器內,trigger 元素跟著容器 scroll,但 fixed popper 是對 viewport,所以 popper 不會跟著容器內部 scroll 飄走——這通常是你要的(tooltip 跟 trigger 對齊),但要意識到行為跟absolute是有差的。 - 這個 modifier 是 Chakra 給的唯一缺口:理論上比較乾淨的做法是 Chakra 把
strategy暴露成 prop。如果未來 Chakra 加了,這段 hack 就可以拆掉。 - 每個 Tooltip 都要傳:modifier 不會自動套到全站。如果想全站套用,可以包一個自己的
<Tooltip>wrapper 預設帶modifiers={tooltipModifiers}。
Related
- Tooltip shouldWrapChildren 的運作原理 — 同一個 Tooltip 的另一個常見坑(hover 事件被 child wrapper 吃掉)
- Parent overflow hidden 切掉 children (portal) — 另一個 portal / popper 相關的定位問題