VIII Wayfinding

Star on GitHubFeedback
VIII WayfindingNative HTML and a hookot-trail

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 trail

Example

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.json

Usage

reading-trail-example.tsx
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 item and, for the rail, scrollbar.
  • Anatomy: <nav class="ot-trail" data-variant="contents | rail" aria-label="On this page"> holding ul.ot-items.ot-trail-list of li.ot-item.ot-trail-step (data-state="read | now | ahead"), each a link a.ot-trail-link (aria-current="location" on the one you are in) with .ot-trail-num and .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 adds p.ot-trail-head (aria-hidden) and a ot-scrollbar with data-variant="page". pinned fixes the place for documentation. With no sections the root is a plain div, 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-top keeps 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

Props of Reading trail
PropTypeDefaultWhat 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.
labelstring"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-topCSS length0pxRail only: keeps the rail and its head clear of a fixed bar at the top of the window.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Reading trailContents pages; SPECTRA's numbersThe section you are in carries the accent and its leader inks as you read it; folios say where each is reached
Rail reading trailA book's spine; Paul Rand's dimension linesThe section you are in runs down beside the page's rail like the title on a spine

How it moves

How it moves
ComponentArticulationWhat moves
Reading trailInkThe current section's leader inks as far as you have read, following the scroll
Rail reading trailTurnThe spine's head rises in when you reach the next section, never on arrival