Sidebar
A column of words beside the work. Folded, it becomes a ruler: a numeral for each group, a tick for each link, and the name comes out into the margin when you point at it.
Report an issue with SidebarExample
Projects
Seven projects, three of them live. Choose a page on the left, or fold the column to a ruler and point at a tick.
Rest
Pointed at
Current
Chapter, closed and opened
Numerals
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, button, scrollbar, and sheet along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/sidebar.jsonUsage
"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Sidebar, SidebarGroup, SidebarHead, SidebarLink, type SidebarProps } from "@/components/ui/sidebar"// Folded, pointing at a tick names it and shows its line.const pages = {Work: { Projects: "Seven of them, three live.", Clients: "Who we work for, and since when.", Journal: "What the studio writes down." },"II Studio": { People: "Nine, and a dog.", Settings: "Hours, billing and who can sign in." },}type Variant = NonNullable<SidebarProps["variant"]>const variants: Variant[] = ["words", "chapter", "numerals"]export default function Example() {const [current, setCurrent] = React.useState("Projects")const [folded, setFolded] = React.useState(false)const [variant, setVariant] = React.useState<Variant>("words")return (<div className="grid gap-10 min-[860px]:grid-cols-[auto_minmax(0,1fr)]"><Sidebar id="studio-app" label="Studio app" sheetLabel="Studio" folded={folded} variant={variant}><SidebarHead>Halden Studio</SidebarHead>{Object.entries(pages).map(([group, names]) => (<SidebarGroup key={group} label={group}>{Object.entries(names).map(([name, line]) => (<SidebarLinkkey={name}href={`#${name.toLowerCase()}`}current={name === current}preview={line}onClick={(e) => {e.preventDefault()setCurrent(name)}}>{name}</SidebarLink>))}</SidebarGroup>))}</Sidebar><div className="grid content-start justify-items-start gap-4"><div role="group" aria-label="Sidebar variant" className="flex flex-wrap gap-x-6 gap-y-2">{variants.map((v) => (<Button key={v} variant="quiet" aria-pressed={v === variant} disabled={folded} onClick={() => setVariant(v)}>{v[0].toUpperCase() + v.slice(1)}</Button>))}</div><Button variant="quiet" aria-expanded={!folded} aria-controls="studio-app" onClick={() => setFolded((f) => !f)}>{folded ? "Unfold the sidebar" : "Fold the sidebar"}</Button><p className="ot-mf">{current}</p><p>Seven projects, three of them live. Choose a page on the left, or fold the column to a ruler and point at a tick.</p></div></div>)}Contract
- Underneath: native, plus a hook; a sheet on narrow screens.
- Anatomy:
<nav class="ot-sidebar ot-scroll">holding.ot-sidebar-list(with.ot-sidebar-head, groups, and a.ot-scrollbaras the nav's last child). A group is.ot-sidebar-label(.ot-sidebar-ithe numeral or initial,.ot-sidebar-restthe remainder) over.ot-sidebar-links; a link is.ot-sidebar-linkholding.ot-sidebar-text.aria-current="page"on the current one.data-foldedfolds the list;data-compactsets it a size down with the links closed up, for a long index. While folded, one.ot-sidebar-callout(aria-hidden,position: fixed,pointer-events: none) is placed beside whatever is pointed at or focused. - Unfolded: words, with a dot before the current page and a ring before the one pointed at. Folded: a ruler about 3rem wide (
--ot-sidebar-rail) on the column's own hairline. Each group keeps its roman numeral ("VI Controls" keeps VI) or else its initial, in pencil (ink for the group you are in); each link is a short tick off the hairline that stretches when pointed at; the current page's tick is the longest, with the accent dot at its far end. Both modes say the same thing: where you are, and what each thing is called. - Pointing at a tick, or focusing it, names it in the margin: a leader line is drawn from the tick to its full name, the group's name beneath, and any
preview(a sentence) after a beat. Pointing at a numeral names the whole group. It stays inside the window, never takes the pointer, and Escape puts it away. The words stay in the page as the link's accessible name, folded or not; the callout is a pointer aid and is hidden from assistive technology. - The words fold away in turn (
--ot-arpeggio) while the ticks grow; with reduced motion it is instant. - Scrolling: the nav is the scroller and takes the scrollbar's rail, which rides the column's own hairline (
--line: 0). Folded ticks are 1.5rem tall so each is a target. varianton the list (data-variant):words(the default),chapterornumerals. Both apply unfolded, in the column and the sheet; folded, every variant is the same ruler.- chapter: a book's running contents, open only at the chapter you are reading. The group holding the current page stands open, its label in ink; every other group keeps only its label. A still pointer (a 240ms wait) opens one under its name, and focus opens it at once, so Tab still reaches every link; leaving closes it. The links stay in the page for assistive technology.
- numerals: "14 Where my feet stand, 08 there i take a photo". Each group's place (01, 02, a CSS counter, drawn and not read) stands large in the margin (
f300, tabular), in pencil, ink for the group you are in or point into. The label, a leading roman numeral and all, and the links sit small beside it (ppoverp), from the numeral's cap height; the numerals share one column (subgrid), so they align. A roman numeral isn't set large: at display size a grotesque's I is a bar. - Keyboard: links (Tab moves tick to tick and shows each name), plus the fold button you give it (
aria-expanded,aria-controlsnaming the nav). Below 860px the same words open in a sheet and never fold.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
Sidebar label | string | Names the nav landmark, such as Studio app or Docs. | |
Sidebar sheetLabel | string | "Index" | Below 860px only a quiet trigger with this word shows, and it opens the same words in a sheet. Choosing a link puts the sheet away. |
Sidebar folded | boolean | false | The column folds to a ruler about 3rem wide: each group keeps its numeral (or initial), each link a tick, the page you are on the longest tick with the dot. Pointing at or focusing a tick draws a leader line to its full name in the margin. The words fold away in turn. Wide only: the sheet never folds. Pair it with your own button and aria-expanded. |
Sidebar variant | "words" | "chapter" | "numerals" | "words" | words: every group open. chapter: only the group holding the current page stands open; the rest keep their names and open for a still pointer, or at once for focus, so Tab still reaches every link. numerals: each group's place (01, 02) set large in the margin, in pencil, ink for the group you are in; its label and links sit small beside it. Folded, every variant is the same ruler. |
Sidebar compact | boolean | false | A size down, with the links closed up: for a long index, such as these docs. |
SidebarHead | p props | The column's own name, at the top. Folded, it keeps its initial too. | |
SidebarGroup label | string | A small pencil label over a list of links, such as a movement or a section. Folded, a leading roman numeral stays ("VI Controls" becomes VI); otherwise the initial. Pointing at it names the whole group. | |
SidebarLink current | boolean | false | Marks the page you are on: aria-current="page", and the one accent dot. |
SidebarLink preview | ReactNode | What appears under the name in the margin while the column is folded and the link is pointed at or focused: a sentence about where it goes. The group's name sits between them. It is a pointer aid; the link's own words are always its accessible name. | |
SidebarLink asChild | boolean | false | Render the child, such as next/link, with the sidebar's look. Plain-text children still fold. |
Scrolling | note | The nav is the scroller: give it a height (or max-height) and it scrolls on the scrollbar's rail, which runs down the column's own rule. | |
Server components | note | Sidebar and its parts can be used from a server component; the state they need lives inside them. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Sidebar | "It has to be design." | Folded, each word keeps its initial |
| Chapter sidebar | A book's running contents | Only the chapter you are in stands open |
| Numerals sidebar | "the silence that heals" (14 / 08) | Each group's number large, its links small beside it |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Sidebar | Fold | The words fold to their initials in turn |
| Chapter sidebar | Open | A still pointer opens a closed group under its name; leaving closes it |
| Numerals sidebar | Ink | Pointing into a group inks its number |