Word relay
A sentence whose last word rolls on every 2.4 seconds, in the italic. Press or use the keys to take over, or pause to hold a word.
Report an issue with Word relayExample
A new word every 2.4 seconds. Press the sentence or use the arrow keys to take over; pause lets it rest.
Autoplaying
Paused
Reduced motion
A slower reading
Press only
Hover
Focus
Disabled
Statement
Statement, hover
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/word-relay.jsonUsage
"use client"import * as React from "react"import { WordRelay } from "@/components/ui/word-relay"import { State, Still } from "@/components/site/state"const ends = ["design.", "type.", "silence.", "yours."]export default function Example() {const [pace, setPace] = React.useState(1)const still = React.useContext(Still)return (<div className="grid w-full grid-cols-[minmax(0,1fr)] justify-items-start gap-(--ot-space-8)"><p className="ot-ff" style={{ margin: 0, fontSize: "clamp(36px, 10vw, var(--ot-ff))" }}><WordRelay autoplay={!still} variant="statement" words={ends}>It has to be</WordRelay></p><div className="grid gap-(--ot-space-3)"><p className="ot-mp" style={{ margin: 0 }}><WordRelay autoplay={!still} words={["slowly.", "at a walk.", "at a run."]} index={pace} onIndexChange={setPace}>Read the page</WordRelay></p><p className="ot-pp" style={{ margin: 0, color: "var(--ot-pencil)" }}>A new word every 2.4 seconds. Press the sentence or use the arrow keys to take over; pause lets it rest.</p></div></div>)}Contract
- Underneath: native
<button>. - Anatomy:
.ot-relay-frameholds the native<button class="ot-relay" data-variant="statement">and the sibling.ot-relay-pauseword button beneath it (never a nested button). The sentence button holds.ot-relay-lead(the sentence, ours),.ot-relay-word(aria-hidden, the word that rolls) and a.ot-srcopy of the chosen word, so the button's name is the whole sentence. Person-driven changes are polite live updates; automatic changes keep the live region off. - sentence (the default): mode-toggle's sentence, for any set of words. The lead is in the roman and graphite; the word is yours, in the expression italic and ink, on a hairline that hugs it. Pressing rolls it on to the next word (out at the top, in from below; back the other way), and the word's box eases from the old width to the new (moderato, breath), so the line follows the word. The last wraps to the first. Autoplay rolls to the next word every
intervalmilliseconds (2400 by default, at least 1000); pressing or using the keys resets that reading interval. - statement: after "It has to be design.", where heavy condensed roman is answered by a large italic that overlaps it, the one accent on the italic word and its full stop. The lead stands heavy (800) and narrow (75%) in ink; the word drops under it half again as large, in the italic and the accent, rising 0.42em into the line above with a halo of the paper round its letters (
--ot-relay-ground), so it cuts the roman where they cross. Pointing sketches a pencil hairline under it. - Props:
words(each with its own punctuation),index/defaultIndex/onIndexChange(index). Controlled relays request the next index for both person-driven and automatic changes; the owner updatesindex. Disabled or single-word relays do not autoplay. - States: rest, hover (sentence: ink, and the hairline inks; statement: the pencil hairline), focus (accent outline), disabled.
- Keyboard: Space or Enter goes to the next word; Down and Right to the next, Up and Left to the one before (Left and Right mirrored right to left); Home and End to the first and the last. Reduced motion: the word changes at once. Forced colours: the hairline and the word in CanvasText; the statement separates its two lines so the forced background cannot cover the lead.
- Autoplay (owner-approved exception, 2026-10-01):
autoplaydefaults to true; false keeps only person-driven behaviour and omits the pause control.defaultPausedstarts it paused. The small roman( pause )/( play )button sits beneath the item at inline start, withpauseLabel/playLabelfor its visible and accessible words. Explicit pause persists until play. Hover, focus within, an off-screen IntersectionObserver entry and a hidden tab suspend it; the person's scroll, drag, key or press takes over, and leaving interaction gives it 1600ms of rest before resuming. Reduced motion disables autoplay entirely and hides the unused control;data-force="reduced"shows that still state in docs. The clock uses rAF time deltas, cancels on unmount and never catches up hidden time. - Autoplay states: autoplaying, paused (play available), reduced motion (still); the pause button is a separate keyboard stop with visible accent focus and native Enter/Space activation.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
autoplay | boolean | true | Play between interactions. Pause on hover, focus within, off-screen or in a hidden tab; resume after 1.6 seconds of rest. Always off under reduced motion. False keeps only person-driven changes and hides the pause control. |
defaultPaused | boolean | false | Start paused with a visible play control. The person can resume it. |
pauseLabel | string | "pause" | The visible word on the pause button, also its accessible name. Parentheses are drawn around it. |
playLabel | string | "play" | The visible word when explicitly paused. Hover and focus still hold autoplay after play is pressed. |
interval | number | 2400 | Milliseconds between words; at least 1000. A person-driven change resets the interval. Controlled relays request the next index through onIndexChange; automatic changes are not live-announced. |
children | ReactNode | The sentence up to the word that changes, ours, in the roman. | |
words | string[] | The words it can end on, in order, each with its own punctuation ("design."). Pressing goes round them; the last wraps to the first. | |
index | number | The chosen word's place, when you hold it (controlled). | |
defaultIndex | number | 0 | The word to start on, when the relay holds it. |
onIndexChange | (index: number) => void | Called with the next index, for both person-driven and automatic changes. A controlled relay moves only when its owner updates index. | |
variant | "sentence" | "statement" | "sentence" | sentence: after mode-toggle's sentence; the word stands in the italic on a hairline that hugs it at the end of the line. statement: after "It has to be design."; the sentence heavy and narrow in ink, and the word half again as large under it, in the italic and the accent, rising into the line above with a halo of the paper so it cuts the roman where they cross. |
--ot-relay-ground | CSS colour | var(--ot-paper) | The halo round the statement's word, if it sits on something other than the paper. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Word relay | Mode toggle's sentence; "It has to be design." (the italic word) | The last word rolls on to the next; the line follows it |
| Statement word relay | "It has to be design." scale contrast | Heavy narrow roman; the chosen word large in the italic and the accent, cutting across it |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Word relay | Roll | Every 2400ms by default, or on press/keys, the word turns over and the line follows its width; pausable, off under reduced motion (owner-approved 2026-10-01) |