Reading trail
Keep your place on a long page: its sections by number and name, set as a contents page, the one you are in carrying the accent while its dotted leader inks as you read. Or the page's own rail, with that section running down it like the title on a spine.
Report an issue with Reading trailExample
The measure
A line of text is read in one breath of the eye. Set it too long and the eye loses its way back to the start of the next; too short, and it spends its time turning. Between forty-five and seventy-five characters, the eye runs out just as the line does.
The measure is set before the size. Choose how wide the column is, then find the size that fills it with about sixty characters, and let the margins take whatever is left.
Leading
The space between lines is named for the strips of lead a compositor laid between rows of type. A longer measure wants more of it, so the eye can find its way back along the gap; a short one can be set nearly solid.
Body text at seventeen pixels reads well at about one and a half times its size. Headings, being short and large, want much less, or their lines drift apart and stop reading as one thought.
The rag
Set flush left, the right edge of a column is ragged, and its shape is part of the page. A good rag moves gently in and out; a bad one makes wedges and steps that the eye stops to look at.
Hyphenation softens it, and so does leaving the last word of a sentence on the line it belongs to. The browser can balance a heading's lines for you, and keep the last line of a paragraph from standing alone.
Widows and orphans
A widow is the last line of a paragraph left alone at the top of a column; an orphan is the first line left alone at the foot. Both break the paragraph's shape where the reader turns, which is where they notice.
On a screen there are no pages to turn, but the same care holds at a column's end, above a picture, or where a section begins.
Colophon
Set in Archivo and Bodoni Moda. The trail beside it reads the page as you scroll: the section you are in carries the accent, and its leader inks as you read it.
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, item, and scrollbar along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/reading-trail.jsonUsage
import { ReadingTrail, type TrailSection } from "@/components/ui/reading-trail"const sections: (TrailSection & { text: string[] })[] = [{id: "trail-measure",name: "The measure",text: ["A line of text is read in one breath of the eye. Set it too long and the eye loses its way back to the start of the next; too short, and it spends its time turning. Between forty-five and seventy-five characters, the eye runs out just as the line does.","The measure is set before the size. Choose how wide the column is, then find the size that fills it with about sixty characters, and let the margins take whatever is left.",],},{id: "trail-leading",name: "Leading",text: ["The space between lines is named for the strips of lead a compositor laid between rows of type. A longer measure wants more of it, so the eye can find its way back along the gap; a short one can be set nearly solid.","Body text at seventeen pixels reads well at about one and a half times its size. Headings, being short and large, want much less, or their lines drift apart and stop reading as one thought.",],},{id: "trail-rag",name: "The rag",text: ["Set flush left, the right edge of a column is ragged, and its shape is part of the page. A good rag moves gently in and out; a bad one makes wedges and steps that the eye stops to look at.","Hyphenation softens it, and so does leaving the last word of a sentence on the line it belongs to. The browser can balance a heading's lines for you, and keep the last line of a paragraph from standing alone.",],},{id: "trail-widows",name: "Widows and orphans",text: ["A widow is the last line of a paragraph left alone at the top of a column; an orphan is the first line left alone at the foot. Both break the paragraph's shape where the reader turns, which is where they notice.","On a screen there are no pages to turn, but the same care holds at a column's end, above a picture, or where a section begins.",],},{id: "trail-colophon",name: "Colophon",text: ["Set in Archivo and Bodoni Moda. The trail beside it reads the page as you scroll: the section you are in carries the accent, and its leader inks as you read it.",],},]export default function Example() {return (<div className="grid w-full grid-cols-[minmax(0,1fr)] items-start gap-x-16 gap-y-10 md:grid-cols-[minmax(0,1fr)_15rem]"><ReadingTrail sections={sections} className="md:sticky md:top-24 md:col-start-2 md:row-start-1" /><article className="ot-prose md:col-start-1 md:row-start-1">{sections.map((s) => (<section key={s.id}><h3 id={s.id}>{s.name}</h3>{s.text.map((t) => (<p key={t.slice(0, 16)}>{t}</p>))}</section>))}</article></div>)}const short = sections.map(({ id, name }) => ({ id: `${id}-s`, name }))const wide = { width: "17rem", maxWidth: "100%" }Contract
- Underneath: hook (reads the window's scroll), composed of
itemand, for the rail,scrollbar. - Anatomy:
<nav class="ot-trail" data-variant="contents | rail" aria-label="On this page">holdingul.ot-items.ot-trail-listofli.ot-item.ot-trail-step(data-state="read | now | ahead"), each a linka.ot-trail-link(aria-current="location"on the one you are in) with.ot-trail-numand.ot-trail-name, the item's leader, and its folio at the end (aria-hidden). Script sets--ot-trail-read(0 to 1, how much of the section you are in is read) on the root. The rail addsp.ot-trail-head(aria-hidden) and aot-scrollbarwithdata-variant="page".pinnedfixes the place for documentation. With no sections the root is a plaindiv, not an empty landmark. - A contents page that reads along with you: each section's number and name, a dotted leader, and where on the page it is reached (00 to 100), as a book's contents give the page. What you've read keeps its leader in ink and its name in pencil; the section you are in has its number in the accent, its name and folio in ink, and its leader inks as far as you've read it; what's ahead waits on the dots. A section counts as reached when its top meets the middle of the window, as the scrollbar marks it; those too near the end to reach it share the last stretch.
- Contents (the default), after contents pages and SPECTRA's numbers-as-data: the list above, for a side column or the head of a page.
- Rail, after the title on a book's spine and the page scrollbar (Paul Rand's dimension lines): the scrollbar already marks where each section begins; the trail adds which one you are in. Its number (accent) and name run down beside the rail, set vertically like a spine, and turn in when you reach the next section. The head steps aside while you point at the rail, where the scrollbar's own numbers arrive. For the keyboard and screen readers, who can't use the rail, the contents stay in the page and come out beside the rail when focus reaches them.
--ot-trail-topkeeps both clear of a fixed bar. The head shows only where the rail does (a fine pointer and scroll timelines). - Keyboard: the links are the page's own anchors. Right to left the leader inks from the right and the rail's head moves to the left edge. Forced colours: the current number is Highlight.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
sections | { id, name, num? }[] | One per section, in page order: the id of the element it begins at, its name, and its number (01, 02 by default). The links are the page's own anchors. | |
variant | "contents" | "rail" | "contents" | contents: a contents page for the margin. Each line is the number, the name, a dotted leader and where on the page the section is reached (00 to 100); what you've read keeps its leader in ink, the one you are in inks its leader as you read, what is to come waits on a rule. rail: the page's scrollbar at the window's edge (the scrollbar's page rail, with a mark per section), and beside it, set vertically like a spine's title, the section you are in. The contents stay in the page for the keyboard and screen readers, and come out beside the rail when focus reaches them. |
label | string | "On this page" | Names the trail for assistive technology, and heads the contents. |
pinned | { now: number; read?: number } | Pins where you are (the section's place, and how much of it is read, 0 to 1) instead of reading the page. For documentation. | |
--ot-trail-top | CSS length | 0px | Rail only: keeps the rail and its head clear of a fixed bar at the top of the window. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Reading trail | Contents pages; SPECTRA's numbers | The section you are in carries the accent and its leader inks as you read it; folios say where each is reached |
| Rail reading trail | A book's spine; Paul Rand's dimension lines | The section you are in runs down beside the page's rail like the title on a spine |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Reading trail | Ink | The current section's leader inks as far as you have read, following the scroll |
| Rail reading trail | Turn | The spine's head rises in when you reach the next section, never on arrival |