VI Controls

Star on GitHubFeedback
VI ControlsNative HTMLot-relay

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 relay

Example

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

Usage

word-relay-example.tsx
"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-frame holds the native <button class="ot-relay" data-variant="statement"> and the sibling .ot-relay-pause word 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-sr copy 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 interval milliseconds (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 updates index. 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): autoplay defaults to true; false keeps only person-driven behaviour and omits the pause control. defaultPaused starts it paused. The small roman ( pause ) / ( play ) button sits beneath the item at inline start, with pauseLabel / playLabel for 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

Props of Word relay
PropTypeDefaultWhat it does
autoplaybooleantruePlay 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.
defaultPausedbooleanfalseStart paused with a visible play control. The person can resume it.
pauseLabelstring"pause"The visible word on the pause button, also its accessible name. Parentheses are drawn around it.
playLabelstring"play"The visible word when explicitly paused. Hover and focus still hold autoplay after play is pressed.
intervalnumber2400Milliseconds 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.
childrenReactNodeThe sentence up to the word that changes, ours, in the roman.
wordsstring[]The words it can end on, in order, each with its own punctuation ("design."). Pressing goes round them; the last wraps to the first.
indexnumberThe chosen word's place, when you hold it (controlled).
defaultIndexnumber0The word to start on, when the relay holds it.
onIndexChange(index: number) => voidCalled 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-groundCSS colourvar(--ot-paper)The halo round the statement's word, if it sits on something other than the paper.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Word relayMode 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 contrastHeavy narrow roman; the chosen word large in the italic and the accent, cutting across it

How it moves

How it moves
ComponentArticulationWhat moves
Word relayRollEvery 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)