Scroll area
A rule appears at an edge only while there's more beyond it, and leaves when you reach the end.
Report an issue with Scroll areaExample
0.4The library
- Every component in one stylesheet, fenced for the library.
- Right-to-left support for crumbs, checkboxes and fields.
0.3Colour
- Four type pairs and five colour schemes.
0.2Motion
- Motion that answers the hand, and rests.
- The overture: text that parts around the pause.
0.1Posters
- Twenty posters pinned to a wall.
- One rule: ours in roman, yours in italic.
0.0A name
- A name, taken from music.
Everything is type. The library gives an interface two typefaces, one accent and a great deal of space to lean on, and takes every picture away.
Space does the layout. Where a box would say that things belong together, the gap between them says it first, and more quietly.
Type is the only ornament. A word can be large or small, roman or italic, ink or pencil, and that is all the decoration there is.
One colour marks where you are. If two things on the page were in the accent, there would be two places to be.
Nothing moves unless you do. The page waits, the way a room waits for someone to speak.
- pppAs soft as it can be played
- ppVery soft
- pSoft
- mpModerately soft
- mfModerately loud
- fLoud
- ffVery loud
- fffAs loud as it can be played
- fpLoud, then at once soft
- sfzA sudden weight on one note
- cresc.Growing louder
- dim.Growing softer
0.4The library
Every component in one stylesheet, fenced for the library.
Right-to-left support for crumbs, checkboxes and fields.
0.3Colour
Four type pairs and five colour schemes.
0.2Motion
Motion that answers the hand, and rests.
The overture: text that parts around the pause.
0.1Posters
Twenty posters pinned to a wall.
One rule: ours in roman, yours in italic.
0.0A name
A name, taken from music.
Everything is type. The library gives an interface two typefaces, one accent and a great deal of space to lean on, and takes every picture away.
Space does the layout. Where a box would say that things belong together, the gap between them says it first, and more quietly.
Type is the only ornament. A word can be large or small, roman or italic, ink or pencil, and that is all the decoration there is.
One colour marks where you are. If two things on the page were in the accent, there would be two places to be.
Nothing moves unless you do. The page waits, the way a room waits for someone to speak.
صفر ديسيبل هو أهدأ صوت يمكن أن يسمعه الإنسان.
المسافة ترتّب الصفحة، والحرف هو الزينة الوحيدة.
لون واحد يدلّ على مكانك، ولا شيء غيره.
لا شيء يتحرّك ما لم تتحرّك أنت.
- pppAs soft as it can be played
- ppVery soft
- pSoft
- mpModerately soft
- mfModerately loud
- fLoud
- ffVery loud
- fffAs loud as it can be played
- pppAs soft as it can be played
- ppVery soft
- pSoft
- mpModerately soft
- mfModerately loud
- fLoud
- ffVery loud
- fffAs loud as it can be played
- Every component in one stylesheet, fenced for the library.
- Right-to-left support for crumbs, checkboxes and fields.
- Four type pairs and five colour schemes.
- Motion that answers the hand, and rests.
- The overture: text that parts around the pause.
- Twenty posters pinned to a wall.
- One rule: ours in roman, yours in italic.
- A name, taken from music.
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and scrollbar along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/scroll-area.jsonUsage
import { ScrollArea } from "@/components/ui/scroll-area"const releases = [{ version: "0.4", name: "The library", notes: ["Every component in one stylesheet, fenced for the library.", "Right-to-left support for crumbs, checkboxes and fields."] },{ version: "0.3", name: "Colour", notes: ["Four type pairs and five colour schemes."] },{ version: "0.2", name: "Motion", notes: ["Motion that answers the hand, and rests.", "The overture: text that parts around the pause."] },{ version: "0.1", name: "Posters", notes: ["Twenty posters pinned to a wall.", "One rule: ours in roman, yours in italic."] },{ version: "0.0", name: "A name", notes: ["A name, taken from music."] },]const idea = ["Everything is type. The library gives an interface two typefaces, one accent and a great deal of space to lean on, and takes every picture away.","Space does the layout. Where a box would say that things belong together, the gap between them says it first, and more quietly.","Type is the only ornament. A word can be large or small, roman or italic, ink or pencil, and that is all the decoration there is.","One colour marks where you are. If two things on the page were in the accent, there would be two places to be.","Nothing moves unless you do. The page waits, the way a room waits for someone to speak.",]const dynamics = [["ppp", "As soft as it can be played"],["pp", "Very soft"],["p", "Soft"],["mp", "Moderately soft"],["mf", "Moderately loud"],["f", "Loud"],["ff", "Very loud"],["fff", "As loud as it can be played"],["fp", "Loud, then at once soft"],["sfz", "A sudden weight on one note"],["cresc.", "Growing louder"],["dim.", "Growing softer"],]function Idea() {return (<div className="grid gap-[var(--ot-space-4)] py-[var(--ot-space-3)] pe-[var(--ot-space-6)]">{idea.map((t) => <p key={t}>{t}</p>)}</div>)}function Dynamics({ rows = dynamics }: { rows?: string[][] }) {return (<ul className="grid gap-[var(--ot-space-2)] py-[var(--ot-space-6)]">{rows.map(([mark, meaning]) => (<li key={mark}><span className="inline-block w-14 tabular-nums">{mark}</span>{meaning}</li>))}</ul>)}/** Ruled: each release is a section, and a mark on the rail. Catchword: the first word below waits on the foot rule. Wheel: the dynamics turn on an arc. */export default function Example() {return (<div className="grid items-start gap-x-(--ot-space-8) gap-y-(--ot-space-7) md:grid-cols-2"><ScrollAreaaria-label="Release notes"sections={releases.map((r) => ({ id: `release-${r.version}`, num: r.version, name: r.name }))}className="max-h-56 max-w-xl md:col-span-2"><div className="grid gap-[var(--ot-space-5)] py-[var(--ot-space-4)] pe-[var(--ot-space-6)]">{releases.map((r) => (<section key={r.version} id={`release-${r.version}`} aria-labelledby={`release-${r.version}-h`}><h3 id={`release-${r.version}-h`}><span className="inline-block w-10 tabular-nums">{r.version}</span>{r.name}</h3><ul className="grid gap-1 ps-10">{r.notes.map((n) => (<li key={n}>{n}</li>))}</ul></section>))}</div></ScrollArea><ScrollArea variant="catchword" aria-label="The idea" className="max-h-56"><Idea /></ScrollArea><ScrollArea variant="wheel" aria-label="Dynamics" className="max-h-56"><Dynamics /></ScrollArea></div>)}Contract
- Underneath: native overflow, with the scrollbar.
- Anatomy:
<div class="ot-scroll" data-slot="scroll-area" data-variant="ruled | catchword | wheel" tabindex="0" role="region" aria-label>holding two rails last (catchword puts<div class="ot-scroll-foot" aria-hidden="true"><span class="ot-scroll-catchword"></span></div>before them): a.ot-scrollbardown the side and a.ot-scrollbar[data-axis="x"]along the bottom, each hidden until there's something to scroll its way. The rail also setsdata-lenis-preventon it while there is something to scroll down, so smooth scrolling leaves it alone. - It wears the page's rail, not a lighter one: 1.5rem wide with the hairline 0.5rem in (
--line), an ink thumb as long as the view.sections({ id, num, name }[], one per element inside with that id) put a tick on both rails: pointing at the rail brings their numbers down it in turn, pointing at a tick names it, pressing one scrolls there (at once with reduced motion), and the section in the middle of the view is ink. A section too near the end to reach the top keeps its own place inside the thumb's last stretch, so the last ticks never pile up. The box is short, so each number leans into it as its tick nears an end (by--at); sideways, the ticks rise from the rail with their numbers above. With no sections it is the bare rail. - A rule appears at an edge only while there's more beyond it. Pure CSS: paper backgrounds that scroll (
background-attachment: local) cover rules that don't. - Catchword, after the printer's catchword (the first word of the next page set alone at the foot of this one, so the reader goes on): while there's more below, a foot line of paper (
.ot-scroll-foot, sticky, 1.625rem, the rule on its lower edge) rides the bottom of the box and the text passes under it; on it, at the far end and clear of the rail, waits the first word whose line runs under the foot, in pencil at--ot-pp. Script (useCatchword) finds it by walking the box's text on scroll, resize, change and font load, skipping anything aria-hidden, and setsdata-idlewhen there is none (at the end the word and the rule go). Each new word turns in (data-turn: rises 0.6em and fades up, allegro, exhale). Pointing inks it; pressing it turns the box so that line stands at the top (smooth, at once with reduced motion) without moving focus. It is a pointer aid, hidden from assistive technology: the word is in the box. - Wheel, after WOVE, where numbers ride an arc, shrinking and fading by their distance from the chosen one: the children of the box's first child (the rows of one list) turn on an arc as the box scrolls. The row in the middle stands at the start edge, full size, in ink; toward the top and bottom the rows fall back along the curve toward the rail by up to
--ot-scroll-arc(--ot-space-7, offset by the square of the distance), shrink to 0.84 and step back through graphite and pencil to the strong rule. Each row runs on its own view timeline (animation-timeline: view()), so the wheel keeps time with the scroll, smooth scrolling included, and nothing moves at rest. The box keeps--ot-scroll-arcclear at its end so no row is cut, and hides sideways overflow. Right-to-left bows the other way. With reduced motion only the ink changes. - Every scroller in the library carries the rail: the command list (so the combobox too), the dialog, sheet and drawer, the thread and the sidebar. Nothing on the site scrolls on the browser's own bar. Code and text areas don't scroll at all (they wrap and grow); the native select's picker and the carousel's track hide the bar.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
aria-label | string | Names what the box holds. It takes focus so the keyboard can scroll it, and a name makes it a region. | |
className | string | Give the box a height or max-height; it scrolls when its content is taller. | |
sections | { id, num, name }[] | Marks on the rail, one per section inside (by its id). Point at the rail and their numbers arrive down it; point at a mark to read its name, press it to scroll there. Sideways content gets the same marks along the bottom. | |
variant | "ruled" | "catchword" | "wheel" | "ruled" | ruled: a rule at an edge while there's more beyond it. catchword, after the printer's: while there's more below, the first word the foot cuts off waits on the foot rule at the far end, and turns in as each new one arrives; press it and the box turns to that line. wheel, after WOVE: the children of what you put inside (the rows of one list) ride an arc as the box scrolls, the one in the middle at the start edge in ink, the rest falling back toward the rail in pencil; with reduced motion only the ink changes. |
Accessibility | note | The box takes focus, so arrow keys, Page Up and Down, Home and End scroll it. The rail and its marks, and the catchword, are pointer aids hidden from assistive technology (the words they repeat are in the box), so give the sections headings too. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Scroll area | "Less is more." rules; Paul Rand dimension lines | A rule only where there's more; the box as a ruler, ticked at each section |
| Catchword scroll area | The printer's catchword | The first word below waits alone on the foot line |
| Wheel scroll area | WOVE | The rows turn on an arc, the middle one in ink |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Scroll area | Arpeggio, then ink | The page rail's: its sections' numbers arrive down the rail (along it, sideways); held, the thumb takes the accent |
| Catchword scroll area | Turn | Each new catchword rises onto the foot line |
| Wheel scroll area | Wheel | The rows ride the arc as you scroll, inking as they pass the middle |