直書側導覽
漢字直書的側邊導覽列:writing-mode 直排、當前項墨線標記
漢字直書(writing-mode: vertical-rl)的側邊導覽列,欄序由右至左如古籍目次。當前項右側以墨筆刷豎線標記,透過 layoutId 彈簧動畫在章節間滑移;hover 時字距微擴、字色加深。搭配 IntersectionObserver 即可追蹤文章捲動位置。
載入預覽⋯
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<CjkVerticalNav />
安裝
npx shadcn@latest add https://webberui.com/r/cjk-vertical-nav.json或在 components.json 設定 registries 後,改用 @webberui/cjk-vertical-nav 安裝。
安裝依賴後,從 registry JSON(/r/cjk-vertical-nav.json 的 files[0].content)複製 cjk-vertical-nav.tsx 原始碼到你的 components/ui/ 目錄:
npm install motion clsx tailwind-merge使用
非受控用法:內部自行維護當前項,點選即切換。
import { CjkVerticalNav } from "@/components/ui/cjk-vertical-nav";
<CjkVerticalNav
items={[
{ id: "prologue", label: "序章" },
{ id: "history", label: "茶史" },
{ id: "tasting", label: "品茗之道" },
]}
defaultActiveId="prologue"
/>受控用法:搭配 IntersectionObserver 追蹤捲動中的章節,把觀察結果回寫到 activeId,墨線便會自動跟著滑移。
const [activeId, setActiveId] = React.useState("prologue");
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) setActiveId(entry.target.id);
}
},
{ rootMargin: "-40% 0px -55% 0px" },
);
document
.querySelectorAll("section[id]")
.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
<CjkVerticalNav
items={items}
activeId={activeId}
onSelect={(id) => {
setActiveId(id);
document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
}}
/>;Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
items | CjkVerticalNavItem[] | — | 導覽項目清單(id 與 label),依直書閱讀順序由右至左排列 |
activeId | string | — | 受控模式:目前作用中的項目 id |
defaultActiveId | string | 第一個項目 | 非受控模式的初始作用項 id |
onSelect | (id: string) => void | — | 點選項目時的回呼 |
ariaLabel | string | "章節導覽" | nav 地標的無障礙名稱 |
className | string | — | 透傳到最外層 nav |
可及性
- 最外層為
nav地標並帶aria-label,螢幕閱讀器可直接跳至導覽區 - 當前項目標記
aria-current="true",輔助科技能得知目前所在章節 - 項目為原生
button,支援 Tab 聚焦與 Enter/Space 觸發,並有清楚的focus-visible外框 - 使用者系統開啟「減少動態效果」時,墨線改為瞬間換位,不做彈簧位移動畫