WebberUI

CJK Vertical Nav

A side navigation bar in vertical Chinese writing — writing-mode vertical typesetting, with an ink line marking the current item.

A side navigation bar in vertical Chinese writing (writing-mode: vertical-rl), with the columns running right to left like the table of contents of a classical book. The current item is marked by an ink-brush vertical line on its right, which glides between sections on a spring animation via layoutId; on hover the letter spacing widens slightly and the text color deepens. Pair it with an IntersectionObserver to track the article's scroll position.

Loading preview…
npx shadcn@latest add https://webberui.com/r/cjk-vertical-nav.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<CjkVerticalNav />

Installation

npx shadcn@latest add https://webberui.com/r/cjk-vertical-nav.json

Or, once registries are configured in components.json, install it as @webberui/cjk-vertical-nav.

Usage

Uncontrolled usage: it maintains the current item itself and switches on click.

import { CjkVerticalNav } from "@/components/ui/cjk-vertical-nav";

<CjkVerticalNav
  items={[
    { id: "prologue", label: "序章" },
    { id: "history", label: "茶史" },
    { id: "tasting", label: "品茗之道" },
  ]}
  defaultActiveId="prologue"
/>

Controlled usage: pair it with an IntersectionObserver to track the section being scrolled through and write the result back to activeId, and the ink line follows along automatically.

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

PropTypeDefaultDescription
itemsCjkVerticalNavItem[]The navigation item list (id and label), arranged right to left in vertical reading order
activeIdstringControlled mode: the id of the currently active item
defaultActiveIdstringfirst itemInitial active item id in uncontrolled mode
onSelect(id: string) => voidCallback when an item is selected
ariaLabelstring"章節導覽"Accessible name for the nav landmark
classNamestringForwarded to the outermost nav

Accessibility

  • The outermost element is a nav landmark with an aria-label, so screen readers can jump straight to the navigation region
  • The current item is marked aria-current="true", so assistive technology knows which section you are in
  • Items are native buttons supporting Tab focus and Enter/Space activation, with a clear focus-visible outline
  • When the user has "reduce motion" enabled at the system level, the ink line changes position instantly with no spring movement animation

On this page