WebberUI

直書側導覽

漢字直書的側邊導覽列: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.jsonfiles[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型別預設值說明
itemsCjkVerticalNavItem[]導覽項目清單(idlabel),依直書閱讀順序由右至左排列
activeIdstring受控模式:目前作用中的項目 id
defaultActiveIdstring第一個項目非受控模式的初始作用項 id
onSelect(id: string) => void點選項目時的回呼
ariaLabelstring"章節導覽"nav 地標的無障礙名稱
classNamestring透傳到最外層 nav

可及性

  • 最外層為 nav 地標並帶 aria-label,螢幕閱讀器可直接跳至導覽區
  • 當前項目標記 aria-current="true",輔助科技能得知目前所在章節
  • 項目為原生 button,支援 Tab 聚焦與 Enter/Space 觸發,並有清楚的 focus-visible 外框
  • 使用者系統開啟「減少動態效果」時,墨線改為瞬間換位,不做彈簧位移動畫

On this page