Progress React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React progress bar, stepped bar or ring for uploads and quotas, determinate or indeterminate, with a label, value, tones and thresholds.
It is read, not operated: it never takes focus. Every value change glides on a spring from where the bar was, with no re-render per frame; the first paint already shows the final value. Without a value the bar slides endlessly (the ring spins), and a value arriving later glides on from there. With thresholds a quota bar turns amber, then red, as it fills. Screen readers hear a progressbar named by the label, described by the description, with the value as a percent. Under reduced motion values jump, stripes stop and the indeterminate bar pulses in place. In the playground, press Simulate to run an import from 0 to 100% in uneven steps. The same markup works on a phone; the header wraps under 240px.
Progress is a free React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add progress, together with useLocale, Motion presets, Number format, Tones and UIProvider and the npm packages @radix-ui/react-progress and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add progressUsage
Import Progress from @/components/ui/progress.
import { Progress, ProgressRing } from "@/components/ui/progress"
export function ClientImport({ done, total }: { done: number; total: number }) {
return (
<div className="flex flex-col gap-4">
<Progress
label="Importing clients"
description={`${done} of ${total} rows`}
value={done}
max={total}
showValue
/>
<Progress
label="Storage"
value={7.2}
max={10}
thresholds={{ warning: 80, danger: 95 }}
showValue
formatValue={(value, max) => `${value} GB of ${max} GB`}
/>
<ProgressRing value={68} tone="brand" showValue aria-label="Monthly goal" />
</div>
)
}Props
valuedefault64number | nullThe current value, from 0 to max. Omit it (or pass null, NaN) for the indeterminate state: a sliding bar or a spinning arc. Values outside 0..max are clamped for drawing. The playground starts at a sample 64; the component's default is null.
maxdefault100numberThe value that means done. 0 or less renders the indeterminate state.
variantdefault"linear""linear" | "segmented" | "ring"linear: one bar. segmented: equal steps (onboarding, password strength, stories); a fractional value fills a segment partly. ring: a circle for KPIs and compact rows.
segmentsdefault5numberSegmented only: how many equal steps. 2,5 of 5 shows two full segments and one half.
sizedefault"md""xs" | "sm" | "md" | "lg"Linear track 2 / 4 / 6 / 8px (xs is the hairline for a refreshing list); ring 24 / 32 / 48 / 64px with a 3 / 3 / 4 / 6px stroke. The xs ring shows its value beside it.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The fill color. The track stays a neutral tint. A finished upload does not turn green on its own: pass success when it should.
colordefault""stringAny CSS color for the fill; replaces the tone. Empty uses the tone.
radiusdefault"full""none" | "sm" | "full"Ends of the linear and segmented bars: square, 2px or a pill.
labeldefault""ReactNodeAbove the bar, or beside the ring with labelPosition side. Names the progressbar for screen readers (else pass aria-label).
descriptiondefault""ReactNodeUnder the bar in small muted text, e.g. "2.4 MB of 12 MB". Linked with aria-describedby.
showValuedefaultfalsebooleanShows the formatted value at the end of the header row (linear) or in the center of the ring.
valueFormatdefault"percent""percent" | "fraction" | "value"How the value reads: "64%", "32 / 50" or "32", in the locale ("64 %" in de-DE).
labelPositiondefault"top""top" | "side"Ring only: top stacks label, ring and description; side puts the text to the right of the ring (KPI rows).
stripeddefaultfalsebooleanLinear only: moving diagonal stripes on the fill while it is below max. They stop under reduced motion.
animateddefaulttruebooleanValue changes glide on a spring. Off, the bar jumps straight to the new value.
localedefault""stringIntl locale for the value. Empty uses the UIProvider locale, then the page language.
thresholds{ warning?: number; danger?: number }Quotas: at or above warning % the fill turns warning, at danger % danger. Compares percentages, not raw values. Wins over tone and color.
bufferdefault0numberLinear only: a lighter second bar behind the fill, in the same units as value (media buffering, uploaded vs processed). Glides on the same spring. Omitted (0 in the playground) draws none.
thicknessdefault0numberRing only: the stroke width in px. 0 uses 3 / 3 / 4 / 6px by size.
formatValue(value: number, max: number) => ReactNodeReplaces valueFormat, e.g. "7.2 GB of 10 GB".
getValueLabel(value: number, max: number) => stringThe aria-valuetext. Defaults to the percent in the locale.
childrenReactNodeRing only: replaces the centered value (an icon, a figure in your own style).
classNamesPartial<Record<"header" | "label" | "value" | "track" | "indicator" | "segment" | "ring" | "description", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last. Other props (aria-label, aria-labelledby, aria-describedby) reach the progressbar.
Parts
ProgressRing
componentThe bare ring for other components (a stat card, an avatar, a file row). Same value props as Progress, children in the center (centered text shrinks to fit) and its own global defaults under progress-ring. Pass it an aria-label.
valuedefault"null"number | nullThe current value; null spins the ring as indeterminate.
maxdefault100numberThe value that fills the ring.
sizedefault"md""xs" | "sm" | "md" | "lg" | number24 / 32 / 48 / 64px, or any px number.
thicknessnumberStroke width in px; 3 / 3 / 4 / 6px by size when left out.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The ring color from the design tokens.
colorstringAny CSS color; replaces the tone.
showValuedefaultfalsebooleanPrints the formatted value in the center.
valueFormatdefault"percent""percent" | "fraction" | "value"How the centered value reads: "68%", "34 / 50" or "34".
animateddefaulttruebooleanValue changes glide on a spring; false jumps.
classNamesPartial<Record<"ring" | "content", string>>Extra classes for the svg ring and the centered content.
Examples
Upload rows
Small bars with the size in MB, a finished file in success and one still processing.
Storage quota
Thresholds at 80% and 95% turn the bar amber, then red. Drag the slider.
Rings as KPIs
labelPosition side puts the label and the description next to the ring.
Segmented onboarding
Four steps with a fraction value; each step glides into the next. Try Back and Continue.
Indeterminate
No value: a sliding bar, a spinning arc and an xs hairline on top of a refreshing card.
Sizes, tones, stripes
xs to lg, every tone, a custom color, stripes with a buffer and rings with an icon.
Files
- components/ui/progress.tsx
FAQ
Is the React Progress component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Progress?
Run npx wingo-ui@latest add progress. It also adds the Wingo UI items it builds on (useLocale, Motion presets, Number format, Tones and UIProvider) and the npm packages @radix-ui/react-progress and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/progress.json.
How does Progress behave on phones?
The same markup works on a phone; the header wraps under 240px.
See also
Used in
More React feedback components
Articles using this component
- Onboarding Checklist UI: The Get Started Card in React
Build it: screens and apps9 min read