Roving Highlight System
單一共享的變形高亮面,可跨 tabs、側欄、網格等容器在任何註冊元素間旅行,內建 roving tabindex 鍵盤導航與焦點環同步。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<RovingHighlightSystem />
安裝
npx shadcn@latest add https://webberui.com/r/roving-highlight-system.json或在 components.json 設定 registries 後,改用 @webberui/roving-highlight-system 安裝。
安裝依賴後,從 registry JSON(/r/roving-highlight-system.json 的 files[0].content)複製 roving-highlight-system.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge使用
把任意容器與其中的 RovingHighlightItem 包在同一個 RovingHighlight 內,整組便共享一面會旅行的高亮。選取跨越不同容器時,高亮面會平滑變形移動到新位置。
import {
RovingHighlight,
RovingHighlightItem,
} from "@/components/ui/roving-highlight-system";
<RovingHighlight defaultValue="overview" variant="solid">
{/* 容器一:分頁列 */}
<div className="flex gap-1">
<RovingHighlightItem value="overview">總覽</RovingHighlightItem>
<RovingHighlightItem value="activity">動態</RovingHighlightItem>
</div>
{/* 容器二:側欄 —— 高亮會從分頁列一路旅行到這裡 */}
<div className="flex flex-col gap-1">
<RovingHighlightItem value="inbox">收件匣</RovingHighlightItem>
<RovingHighlightItem value="starred">已加星</RovingHighlightItem>
</div>
</RovingHighlight>;受控模式
傳入 value 與 onValueChange 即進入受控模式,選取狀態由外部掌握:
const [value, setValue] = React.useState("overview");
<RovingHighlight value={value} onValueChange={setValue}>
{/* ... */}
</RovingHighlight>;焦點跟隨選取
selectOnFocus 開啟後,方向鍵移動焦點的同時即選取(自動模式),高亮直接跟著焦點走:
<RovingHighlight defaultValue="overview" selectOnFocus>
{/* ... */}
</RovingHighlight>Props
RovingHighlight
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | string | null | — | 受控選取值 |
defaultValue | string | null | null | 非受控初始選取值 |
onValueChange | (value: string) => void | — | 選取變更時觸發 |
variant | "solid" | "outline" | "underline" | "solid" | 高亮款式:填色面/外框環/底線 |
orientation | "horizontal" | "vertical" | "both" | "both" | 方向鍵導航方向 |
loop | boolean | true | 導航到端點時是否循環回另一端 |
selectOnFocus | boolean | false | 焦點移到項目時是否同步選取 |
children | React.ReactNode | — | 任意容器與其中的 RovingHighlightItem |
RovingHighlightItem
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
value | string | — | 此項目的唯一識別值(整組不可重複) |
disabled | boolean | false | 停用:不可選取、跳過鍵盤導航、不成為 tab 停靠點 |
onSelect | (value: string) => void | — | 被選取時觸發(點擊或鍵盤 Enter/Space) |
children | React.ReactNode | — | 項目內容(文字、圖示等) |
其餘 button 原生屬性(aria-label、onClick 等)會一併轉發到底層元素。
細節
- 跨容器旅行:整組同時只有被選取的項目渲染高亮面,切換時舊位置卸載、新位置掛載,由 Motion 的
layoutId共享佈局動畫接手,讓高亮跨越不同容器平滑變形移動。 - 命名空間隔離:每個
RovingHighlight以LayoutGroup為layoutId命名,同頁多個實例彼此的高亮不會互相配對。 - 三款高亮:
solid填色面、outline外框環、underline底線,皆會旅行。
可及性
- roving tabindex:整組僅有一個 tab 停靠點(焦點 > 選取 > 第一個可用項目),
Tab進入後以方向鍵在所有已註冊項目間漫遊,Home/End跳至首尾,Enter/Space選取。 - 焦點環同步:以近似
:focus-visible的偵測,只有鍵盤操作時才顯示焦點環;焦點環同樣是共享元素,會隨焦點跨容器變形移動。 - 選取項目帶
aria-current="true",停用項目帶aria-disabled並排除於鍵盤導航之外。 - 使用者系統開啟「減少動態效果」時,高亮與焦點環改為直接出現在新位置,不再旅行。