VIII Wayfinding

Star on GitHubFeedback
VIII WayfindingNative HTMLot-rows

Rows

An index of hairline-separated rows. The one you point at reverses out of ink and steps forward with an arrow; or repeats are set as ditto marks; or a ring before each row fills once you have been there.

Report an issue with Rows

Example

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.

npx shadcn@latest add https://thedirectors.agency/ui/r/rows.json

Usage

rows-example.tsx
import { Row, RowKind, RowMeta, RowTitle, Rows, type RowsProps } from "@/components/ui/rows"const work = [["Halden", "Identity", "2026"],["Northlight", "Web", "2026"],["Oda Studio", "Identity", "2025"],["Tidewater", "Motion", "2025"],["Marram", "Web", "2025"],]// By year, then kind: the order a ledger keeps, so the repeats fall together.const byYear = [["Halden", "Identity", "2026"],["Kestrel", "Identity", "2026"],["Northlight", "Web", "2026"],["Oda Studio", "Web", "2025"],["Marram", "Web", "2025"],["Tidewater", "Motion", "2025"],]function Index({ variant, label, here, rows = work }: { variant?: RowsProps["variant"]; label: string; here?: string; rows?: string[][] }) {return (<Rows variant={variant} aria-label={label}>{rows.map(([title, kind, year]) => (<Row key={title} href={`#${title.toLowerCase().replace(" ", "-")}`} aria-current={title === here ? "page" : undefined}><RowTitle>{title}</RowTitle><RowKind>{kind}</RowKind><RowMeta>{year}</RowMeta></Row>))}</Rows>)}export default function Example() {return (<div className="grid w-full" style={{ gap: "var(--ot-space-7)" }}><Index label="Projects" /><div className="grid gap-3"><span className="ot-label">Ditto</span><Index variant="ditto" label="Projects by year" rows={byYear} /></div><div className="grid gap-3"><span className="ot-label">Trail: open one, and its ring fills</span><Index variant="trail" label="Projects you have seen" here="Oda Studio" /></div></div>)}const one = (props: { force?: string; current?: boolean }) => (<Row href="#halden" data-force={props.force} aria-current={props.current ? "page" : undefined}><RowTitle>Halden</RowTitle><RowKind>Identity</RowKind><RowMeta>2026</RowMeta></Row>)

Contract

  • Underneath: native list.
  • Anatomy: <ul class="ot-rows" data-variant="ditto | trail"> with <li><a class="ot-rows-link">, holding .ot-rows-title, .ot-rows-kind, .ot-rows-meta, and the → drawn after them (empty alt text).
  • reverse (the default, no attribute). On hover or focus, the row reverses: an ink block rolls on and the text turns to paper. The title steps 8px forward and the arrow comes in. Script sets data-edge="top | bottom" on pointer enter and leave, so the ink comes in from the side your hand entered and leaves toward the side it goes. Moving down the list, one block of ink seems to travel with you.
  • ditto: the ledger's and the typed list's ditto mark. A kind or a meta (a year) that repeats the row above is set as a ditto mark (″, pencil, centred over the word it stands for), so only what changes is written out and the marks make a rhythm down the columns, the barcode rhythm of SPECTRA. Rows works the repeats out after every render (data-ditto on the cell) and the words stay in the page, so a screen reader hears every row in full. Pointing at or focusing a row spells its dittos back into words (allegro); no ink block, the title steps and the arrow comes in. Sort the rows so the repeats fall together.
  • trail: after "28 December", the index keeps count of where you've been. A ring hangs in the margin before each row (space-3, stepping in below 40rem): a hairline ring in rule-strong for a row not yet opened, filled with ink once the browser has visited the link (:visited, which only allows colours, so the ring's paper fill is opaque and nothing is stored), the accent for the row with aria-current (where you are, the one accent). Pointing inks the ring's line and it grows a quarter with spiccato. No ink block. Forced colours draw it in CanvasText, VisitedText and Highlight.
  • Narrow (below 40rem) the title takes its own line and the kind and meta sit under it, a space-1 below, so a long title never runs into its kind.
  • Keyboard: Tab through the links; focus shows what pointing shows.

Props

Props of Rows
PropTypeDefaultWhat it does
Rows variant"reverse" | "ditto" | "trail""reverse"reverse: the row you point at reverses out of ink. ditto: a kind or a year that repeats the row above is set as a ditto mark (″) in pencil, worked out after every render; pointing at or focusing a row spells it out. trail: a ring hangs before each row (stepping in below 40rem): a hairline ring for a row not yet opened, ink once the browser has visited the link (:visited, so nothing is stored), the accent for the row with aria-current. Pointing inks the ring's line.
Row aria-current"page" | …Set on the link. trail marks it with the accent: where you are.
Row hrefstringMakes the row a link. Without it, or asChild, the row is plain and doesn't react.
Row asChildbooleanfalseRender your own link (Next's Link, say) inside the row with the row's look.
RowTitlespanThe name, set large.
RowKindspanWhat it is, in graphite.
RowMetaspanThe quiet fact at the end: a year, a count.
Row onClickMouseEventHandler<HTMLElement>Passed to the inner link, slotted child, or plain div. Use href for navigation or asChild with a button for a keyboard-accessible action.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Index rows"the uncreative"The row reverses out of ink
Ditto rowsThe ledger's ditto mark; SPECTRA's barcode rhythmWhat repeats the row above is a ditto mark; pointing spells it out
Trail rows"28 December" dotsA ring per row fills with ink once you've been there; where you are is the accent

How it moves

How it moves
ComponentArticulationWhat moves
Index rowsRollerThe ink follows the pointer in and out
Ditto rowsSpell outThe ditto marks turn back into words, allegro
Trail rowsSpiccatoPointing inks the ring and it grows a quarter, landing with one rebound