Feedback
Progress Meter
One progress contract, two geometries — a linear bar or a compact ring, both with determinate and indeterminate modes, four tones and four sizes.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/progress-meter.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ProgressMeter" component (no
dependencies beyond React; pure markup + CSS, no state and no timers, so it
works inside a server component).
Contract
- Export a forwardRef div extending React.HTMLAttributes<HTMLDivElement>.
- Props:
- shape = "bar" | "ring" (default "bar") — the only thing that changes is how
the value is drawn; the accessibility contract is identical.
- value?: number — omit for an indeterminate meter; out-of-range values are
clamped into 0..max.
- max = 100 (guard against max <= 0 by falling back to 100).
- size = "xs" | "sm" | "md" | "lg" (default "md"). Bar: track height
(h-1 / h-2 / h-3 / h-4). Ring: outer diameter (48 / 64 / 96 / 128 px) with
a default stroke that scales with it (5 / 6 / 8 / 10) so a small ring never
reads as a donut. Document this dual meaning in the prop's JSDoc.
- tone = "default" | "success" | "warning" | "destructive" (default
"default") — bg-primary / bg-chart-2 / bg-chart-4 / bg-destructive for the
bar fill, and the stroke-* equivalents for the ring arc.
- label?: string — bar: the title on the left of the row above the track;
ring: custom center content that wins over the percentage (e.g. "3/4").
- showValue?: boolean — the rounded percentage. Defaults to true for rings
and false for bars (a ring's center would otherwise be empty).
- striped?: boolean — diagonal texture on the bar fill; the ring ignores it.
- strokeWidth?: number — ring only, overriding the size-derived default.
- className merges onto the root for BOTH shapes; the fill and the arc carry
data-slot="fill" / data-slot="arc" so a consumer can target them in CSS.
- Both shapes render role="progressbar" with aria-valuemin={0},
aria-valuemax={max}, aria-valuenow (omitted while indeterminate),
aria-busy while indeterminate, and aria-label falling back to
label ?? "Progress". Build this attribute set once and spread it into
whichever shape renders, so the two branches can never drift.
Behavior
- Determinate bar: the fill's width is the clamped percentage, transitioned
(width, 300ms ease-out).
- Indeterminate bar: a half-width fill sweeps left↔right on an alternating
keyframe.
- Determinate ring: an SVG with a muted track circle plus an arc circle whose
stroke-dasharray is the circumference and whose stroke-dashoffset is
circumference * (1 - percent/100), transitioned (stroke-dashoffset, 500ms).
Rotate the svg -90deg so 0% starts at twelve o'clock; strokeLinecap="round".
- Indeterminate ring: keep a fixed quarter-circumference dash
(`${c*0.25} ${c}`) at offset 0 and rotate the whole svg 360deg on a loop.
- Reduced motion: never remove the meter. Drop the sweep/spin animations and
the transitions (motion-reduce:[animation:none] /
motion-reduce:transition-none) — the value still shows, the aria contract is
unchanged, only the movement stops.
- Ship both keyframes (bar sweep, ring spin) in ONE React 19 hoisted
<style href precedence="medium"> tag; several meters on a page dedupe to a
single style tag.
Rendering & styling
- Semantic tokens only: bg-muted / stroke-muted for the track, the tone map
above for the fill and arc, text-muted-foreground for the percentage,
tabular-nums so the number never jitters as it counts.
- The stripe texture is a repeating-linear-gradient built from
color-mix(in oklab, var(--background) 34%, transparent), so it reads on any
fill color in both light and dark schemes without a hardcoded value.
- Bar root is w-full (it fills its column); ring root is
relative inline-flex shrink-0 with explicit width/height, so it behaves like
an icon in a flex row.
Customization levers
- Shape is the headline lever: the same value/tone/label props render either
geometry, so a dashboard can show a bar in a list row and a ring in a KPI
tile without a second component.
- Thresholds: pick `tone` from the value on the consumer side (e.g. >90%
destructive, >75% warning) — the component deliberately does not encode
policy, so quota rules stay in your app.
- Density: `size` covers the common range; for a bespoke ring pass
strokeWidth, or set width/height via className and let the svg follow.
- Ring center: `label` accepts any short string — steps ("3/4"), remaining
budget ("12 GB"), or a letter grade; pass showValue={false} to leave it bare.
- Bar row: omit both `label` and showValue to get a naked track for use inside
a table cell or a card footer.Concepts
- One contract, two geometries — a bar and a ring answer the same question ("how far along?") and report the identical
role="progressbar"value set; only the drawing differs, so switchingshapeis a layout decision rather than a rewrite. - Determinate vs indeterminate — omitting
valueis the switch: the meter stops claiming a number, dropsaria-valuenow, and raisesaria-busyso assistive tech hears "busy" instead of a fake percentage. - Size means edge or diameter — the same enum sets a bar's track height and a ring's outer diameter (with the stroke scaled from it), which keeps one prop for "how big" without pretending the two shapes measure the same thing.
- Clamped value, honest label — out-of-range values are clamped and the printed percentage is derived from the clamp, so a buggy 140 can never render a 140%-wide fill or a label that disagrees with the arc.
- Tone carries policy, not the component — quota thresholds ("red past 90%") live in the consumer's
tonechoice; the meter ships no built-in policy, which is what lets it serve uploads, quotas and checklists alike. - Reduced-motion honesty — under
prefers-reduced-motionthe sweep, the spin and the width/offset transitions are dropped, but the value and the ARIA contract stay exactly the same: information is never carried by motion alone.
Spinner
One indeterminate loader, seven shapes — three boxed (ring, dual-ring, bars) and four inline dot rows (dots, ellipsis, bounce, wave), colored by a single tone prop or plain currentColor.
Banner
A page-top announcement bar with info, accent and destructive variants, a real action link and consumer-owned dismissal.