VII Signals

Star on GitHubFeedback
VII SignalsNative HTMLot-dots

Spinner

The wait, written the way a score writes it: periods, a round, a metronome, a fermata, a breath, a rising chord, or the word itself being inked.

Report an issue with Spinner

Example

Loadingword

Saving your draft

Checking the addressdots

Checking the address

Syncing the libraryround

Syncing the library

Waiting for the printermetronome

Waiting for the printer

Holding your placefermata

Holding your place

Connectingbreath

Connecting

Rendering the previewarpeggio

Rendering the preview

Word, reduced motionLoading
Dots, reduced motion
Round, reduced motion
Metronome, reduced motion
Fermata, reduced motion
Breath, reduced motion
Arpeggio, reduced motion
In a busy button

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

Usage

This example also uses Button.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json
spinner-example.tsx
import { Button } from "@/components/ui/button"import { Spinner, type SpinnerProps } from "@/components/ui/spinner"// Each wait with the words it would stand beside. Alone, a spinner says its label to a screen reader;// in a sentence the words say it, so the sentence is the status and the spinner stays silent.const waits: { variant: NonNullable<SpinnerProps["variant"]>; words: string }[] = [{ variant: "word", words: "Saving your draft" },{ variant: "dots", words: "Checking the address" },{ variant: "round", words: "Syncing the library" },{ variant: "metronome", words: "Waiting for the printer" },{ variant: "fermata", words: "Holding your place" },{ variant: "breath", words: "Connecting" },{ variant: "arpeggio", words: "Rendering the preview" },]export default function Example() {return (<div className="grid grid-cols-[repeat(auto-fill,minmax(8.5rem,1fr))] gap-x-6 gap-y-12 self-stretch sm:grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] sm:gap-x-10 sm:gap-y-16">{waits.map(({ variant, words }) => {const cut = words.lastIndexOf(" ") + 1return (<div key={variant} className={variant === "word" ? "col-span-2 grid content-start gap-3" : "grid content-start gap-3"}><Spinner variant={variant} size="l" label={variant === "word" ? undefined : words} /><span className="ot-label">{variant}</span>{variant === "word" ? (<p className="ot-p"><Spinner variant="word" label={words} /></p>) : (<p className="ot-p" role="status">{/* The last word and the spinner never part at a line break. */}{words.slice(0, cut)}<span className="whitespace-nowrap">{words.slice(cut)} <Spinner variant={variant} /></span></p>)}</div>)})}</div>)}

Contract

  • Underneath: native.
  • Anatomy: <span class="ot-dots" data-variant="…" aria-hidden="true"><span class="ot-dots-art" aria-hidden="true"><i></i>…</span></span>. The default, dots, is what a busy button holds, beside its present-tense word ("Saving"). Given a label, the root is instead role="status" (polite) with the label in a .ot-sr. Optional data-size="l" stands it alone at the f step; otherwise everything is in em and it takes the size of its line.
  • Shown only while something the person started is running: the one thing that moves by itself, so every loop is slow and eased like breathing. The art stands on the baseline in a box a capital tall, so it reads as one more glyph; strokes stay hairlines at any size.
  • The family, each a way a score or a page writes a wait:
    • dots (default): three periods on the baseline, each lifting as it brightens, in turn.
    • round: three voices on one hairline ring, each entering a beat after the last, as a round's voices do; they gather at the top and the foot and spread between.
    • metronome: a stroke rod on a hairline foot, a dot for its weight, swinging a slow beat.
    • fermata: the hold. The arc draws in the reading direction (mirrored right to left), the dot lands under it with spiccato, the pause is held, then the arc lifts away the way it was drawn.
    • breath: a dot breathes in and opens into a hairline ring, holds, and closes into a dot again.
    • arpeggio: four notes of a chord rising (a third, a third, a fourth), struck in turn with spiccato and let ring.
    • word: the label itself in the voice, pencilled; the ink runs through it letter by letter and leaves in the same order. It is always read.
  • Reduced motion: nothing travels. Each stops on a frame that still reads as unfinished (the periods trail off, the voices stand apart, the sign of the hold, a small ring, the chord rising in strength, the first letters inked) and the whole mark dims and returns, 2800ms each way, so it still reads as busy. data-force="reduced" pins that in the docs.
  • Forced colours: the marks keep the forced colour of the words around them (ButtonText in a button); the word pencils in GrayText.

Props

Props of Spinner
PropTypeDefaultWhat it does
variant"dots" | "round" | "metronome" | "fermata" | "breath" | "arpeggio" | "word""dots"dots: three periods lift in turn. round: three voices chase round a ring. metronome: a pendulum swings a slow beat. fermata: the hold is drawn, held and lifted. breath: a dot opens into a ring and closes. arpeggio: four notes rise, struck in turn. word: the label, inked letter by letter.
size"m" | "l""m"m takes the size of the text around it; l stands alone at the section-heading size.
labelstringLeave it out when the words beside it say what's happening ("Saving"), as in a busy button: the spinner stays silent. Given, it's a polite status read once. word writes it, "Loading" by default, and is always read.
classNamestringMerged onto the root span. Show it only while something the person started is running.

Craft

How it moves

How it moves
ComponentArticulationWhat moves
LoadingBreathThe periods lift as they brighten
RoundCanonThree voices chase round the ring, gathering and spreading
MetronomeBreathThe rod swings a slow beat
FermataHeldThe arc draws, the dot lands, the hold, the arc lifts
BreathBreathThe dot opens into a ring and closes
ArpeggioArpeggioThe notes rise, struck in turn, and ring
WordWrittenThe ink runs through the word and leaves