Progress
How far something you started has got: a hairline and an italic percentage, the words themselves inking in, one ring per item, a pair of parentheses closing on the words, or a tally. Done, a full stop lands.
Report an issue with ProgressExample
hairline
sentence
count
parentheses
tally
Hairline, not started
Halfway
Done
Unknown
Sentence, not started
Halfway
Done
Unknown
Count, not started
Halfway
Done
Unknown
Parentheses, not started
Halfway
Done
Unknown
Tally, not started
Seven of twelve
Done
Unknown
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and fraction along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/progress.jsonUsage
This example also uses Button.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ button.json"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Progress } from "@/components/ui/progress"const FILES = 12type Phase = "ready" | "preparing" | "uploading" | "done"// The action keeps its name all the way through: upload, uploading, uploaded.const words: Record<Phase, [string, string, string]> = {ready: ["12 photographs ready to upload", "Twelve photographs from", "Photographs to upload"],preparing: ["Preparing 12 photographs", "Preparing twelve photographs from", "Preparing photographs"],uploading: ["Uploading 12 photographs", "Uploading twelve photographs from", "Uploading photographs"],done: ["Uploaded 12 photographs", "Uploaded twelve photographs from", "Uploaded photographs"],}// One upload, told five ways. It prepares while nobody knows how long it will take, then uploads, then says it's done.export default function Example() {const [phase, setPhase] = React.useState<Phase>("ready")const [p, setP] = React.useState(0)const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)React.useEffect(() => () => clearTimeout(timer.current), [])const upload = () => {setPhase("preparing")setP(0)timer.current = setTimeout(() => {setPhase("uploading")let v = 0const step = () => {v = Math.min(1, v + 0.006 + Math.random() * 0.03)setP(v)if (v < 1) timer.current = setTimeout(step, 120)else setPhase("done")}step()}, 1800)}const value = phase === "preparing" ? null : pconst [short, long, counted] = words[phase]const busy = phase === "preparing" || phase === "uploading"return (<div className="grid w-[min(36rem,100%)] justify-items-start gap-(--ot-space-7)"><div className="grid w-full gap-4"><span className="ot-label">hairline</span><Progress value={value === null ? null : value * 100} label={short} /></div><div className="grid w-full gap-4"><span className="ot-label">sentence</span><Progressvariant="sentence"value={value === null ? null : value * 100}label={<>{long} <span className="ot-yours">Lisbon, in April</span></>}/></div><div className="grid w-full gap-4"><span className="ot-label">count</span><Progress variant="count" value={value === null ? null : Math.floor(value * FILES)} max={FILES} label={counted} /></div><div className="grid w-full gap-4"><span className="ot-label">parentheses</span><Progress variant="parentheses" value={value === null ? null : value * 100} label={short} /></div><div className="grid w-full gap-4"><span className="ot-label">tally</span><Progress variant="tally" value={value === null ? null : Math.floor(value * FILES)} max={FILES} label={counted} /></div><Button disabled={busy} onClick={upload}>{phase === "done" ? "Upload again" : "Upload"}</Button></div>)}Contract
- Underneath: native
<progress>, named by a native<label>(oraria-label="Progress"without one). Leave outvalue(or passnull) and it is indeterminate. - Anatomy:
.ot-progress(data-variant,data-state="loading|complete|indeterminate") holding.ot-progress-label, the variant's figure (aria-hidden), and<progress>. One number,--ot-progress-p(0–1, a registered property), is eased at andante, so the fill, the numeral and the ink glide together. - The label keeps the action's name: "Uploading 12 files", then "Uploaded 12 files". It carries no full stop; every variant ends the same way: when it's done, a full stop lands (spiccato).
hairline(default): a line fills in ink toward a ring, and the percentage,.ot-progress-value, is set as a large italic numeral that ticks through every number on its way (a CSS counter on the eased value, rounded down, so it never says 100 early). Done, the ring fills into a full stop.sentence: the words are the bar. The label is set large and inks in from pencil, in reading order, with a soft wet edge, sliced across lines so a wrapped sentence inks line after line. Yours (italic) inks in with it. Done, a full stop drops onto the baseline. The native bar is visually hidden.count: for things you can count. Aot-fraction(count of total) rolls as each item lands, and.ot-progress-unitsshows one ring per item, like days on the calendar: done is a dot (data-done, spiccato pop), the one under way an ink ring (data-now), the rest pencil rings. Up to 100 rings; past that each ring stands for a share. Screen readers hear "4 of 8" (aria-valuetext).parentheses: from 20(25). The label stands in the middle of the measure between two thin parentheses (.ot-progress-paren, weight 100), and the work still to do is the silence inside them. Two.ot-progress-restspacers share the free space by (1 − p) (flex-grow, fed by the eased number), so as the work goes the parentheses close on the words from both ends; done, they hold them and the full stop lands inside. Flex, so right to left mirrors, and the glyphs mirror themselves. The native bar is visually hidden.tally: how a hand counts. Aot-fractionas in count, and.ot-progress-tally: one stroke per item in gates of five (<b>), four standing and the fifth crossing them, so the count reads in fives at a glance. To come, a stroke is a pencil hairline; as its item lands, ink draws down it (the fifth, across it); the next one waits in ink (data-now). Done, a dot lands after the last gate. Up to 100 strokes. Screen readers hear "7 of 12".- Indeterminate: the number stands back and a stroke reads along, only while it runs: a segment passes along the line, a band of ink passes through the words, the rings and the strokes ink in turn, and the parentheses listen, drawing a little way in and back. Reduced motion: no pass, a still rule, the parentheses at the ends. Right to left: the line fills and the words ink from the right; the gates mirror.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "hairline" | "sentence" | "count" | "parentheses" | "tally" | "hairline" | Hairline is a line that fills toward a ring while an italic percentage ticks up. Sentence is the label itself, inking in word by word in reading order. Count is a fraction of items and one ring per item, for things you can count (files, messages). Parentheses sets the label between two thin parentheses at the ends of the measure: the work still to do is the silence inside them, and they close on the words as it goes. Tally is a fraction and one stroke per item, gated in fives the way a hand counts. |
value | number | null | How much is done, out of max. Leave it out (or pass null) while nobody knows yet: the bar is indeterminate and a stroke reads along it. | |
max | number | 100 | The total. The hairline shows value as a percentage of it; a count shows one ring per item and a tally one stroke per item, up to 100. |
label | ReactNode | What is being done, without a full stop, and keeping the action's name: Uploading 12 files, then Uploaded 12 files. It names the bar through a native label; without it the bar is named Progress. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Progress | Dot calendar | A ring per item fills; the words ink in; done lands a full stop |
| Parentheses progress | 20(25) | The work still to do is the silence inside the parentheses |
| Tally progress | The hand's tally; Basquiat sheet | One stroke per item, the fifth crossing the four |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Progress | Andante, then spiccato | The line, the ticking italic numeral and the inked words glide on one eased value; each ring and the closing full stop pop as they land |
| Parentheses progress | Closing | The parentheses glide in on the words from both ends on the eased value; unknown, they draw a little way in and back |
| Tally progress | Written | Ink draws down each stroke as its item lands, and across the fifth; the full stop lands after the last gate |