React Progress Bar Component Examples
Copy-paste React Progress examples: upload bar, multi-step form, page-scroll indicator, password strength, and custom max. Zero runtime deps. TypeScript only.
Reach for a progress bar when the UI has a known total and needs to say how far through it you are — bytes uploaded out of a file size, step three of five in a checkout, forty percent down a long article. Drivn's Progress does that and nothing more. It renders a w-full h-2 bg-accent rounded-full overflow-hidden track wrapping a bg-primary bar, and sets that bar's width from a percentage clamped into [0, 100] with Math.min(100, Math.max(0, (value / max) * 100)).
The file is forty lines at @/components/ui/progress.tsx. It imports react for the type namespace and the local cn() helper, and stops there — no Radix, no portal, no hooks. There is no 'use client' directive either, which means the bar drops into a Server Component and renders at request time. Any 'use client' you see below belongs to the example around it, not to Progress. Every snippet is TypeScript, because Drivn ships TypeScript-only, and every import points at the path the Drivn CLI installs under.
Five call sites follow: a fixed-value bar, an upload bar that ticks in real time, a multi-step form indicator driven by a custom max, a scroll-position indicator wired to the window scroll event, and a password-strength meter scored from a rule set. Props and the full source live on the Progress docs page; the Radix-backed alternative is covered on Drivn vs shadcn Progress. Fresh install steps are in Installation.
Default fixed-value bar
The shortest possible Progress. Pass a value between 0 and 100 to render the bar at that fixed percentage. The component computes pct = Math.min(100, Math.max(0, (value / max) * 100)) and writes the result into the inner <div> width via the style prop. The default max is 100, so <Progress value={65} /> renders a bar that fills 65 percent of the track. The transition class on the bar (transition-all duration-300 ease-out) means subsequent value changes animate over 300 milliseconds with an ease-out curve.
This is the right starting shape for a static dashboard widget (project completion at 72 percent), a profile completion indicator, a quota meter that shows the current usage of a known cap, and any other "here is the current value, no live updates" case. The component is a Server-Component-safe import because the registry source ships without 'use client' — meaning the bar can sit inside a Next.js app/ page that fetches the value at request time without forcing the surrounding tree onto the client. For an interactive sibling that lets the user set the value, see the Drivn Slider component.
1 import { Progress } from "@/components/ui/progress" 2 3 export default function ProfileCompletion() { 4 return ( 5 <div className="space-y-2"> 6 <div className="flex justify-between text-sm"> 7 <span>Profile completion</span> 8 <span className="text-muted-foreground">72%</span> 9 </div> 10 <Progress value={72} /> 11 </div> 12 ) 13 }
Animated upload progress bar
For an upload bar that ticks in real time, hold the percentage in a React.useState and update it from the upload progress event. The example below simulates the cadence with a setInterval that increments the value by 4 every 200 milliseconds and clears when the value hits 100. In production, replace the interval with the actual upload progress source — a fetch upload progress event, an XHR onprogress callback, a tus-js-client progress handler, or whatever the storage SDK exposes. The Drivn Progress bar transitions between consecutive values through its built-in transition-all duration-300 ease-out, so the perceived motion is smooth even when the underlying source ticks discretely.
The 'use client' directive on this example file is necessary because the call site itself uses React.useState and React.useEffect — the Progress component does not force the boundary, the surrounding example does. For a cleaner separation, wrap the upload logic in a custom useUploadProgress hook that lives in a 'use client'-marked utility file and import the hook from the surrounding component. See the upload pattern shape used on Drivn vs shadcn Progress for the transform-vs-width trade-off discussion that matters for very high update frequencies.
1 'use client' 2 import * as React from 'react' 3 import { Progress } from "@/components/ui/progress" 4 5 export default function UploadBar() { 6 const [pct, setPct] = React.useState(0) 7 8 React.useEffect(() => { 9 const id = setInterval(() => { 10 setPct((p) => (p >= 100 ? (clearInterval(id), 100) : p + 4)) 11 }, 200) 12 return () => clearInterval(id) 13 }, []) 14 15 return ( 16 <div className="space-y-2"> 17 <div className="flex justify-between text-sm"> 18 <span>Uploading project.zip</span> 19 <span className="text-muted-foreground">{pct}%</span> 20 </div> 21 <Progress value={pct} /> 22 </div> 23 ) 24 }
Multi-step form indicator with custom max
The max prop exists for exactly this. Pass the current step as value and the total count as max, and the component handles the division — step 2 of 5 renders at 40 percent, step 4 of 5 at 80. Doing it this way rather than computing (step / total) * 100 at the call site pays off when the wizard changes shape: adding a sixth step means editing one number in one place instead of hunting down every percentage expression that hardcoded five. The same shape covers tasks-completed-out-of-total, lessons in a course, and any other discrete counter with a fixed ceiling.
Put a text label above the bar — Step 2 of 5 — reading from the same state the bar reads from, so the two cannot drift apart. The bar already announces itself through aria-valuenow and aria-valuemax, but a visible label hands sighted users the discrete count instead of a percentage they have to reverse-engineer from a filled width.
Progress is the passive half of this pattern. When the steps should be clickable — jumping back to shipping details from the payment screen — reach for the Drivn Stepper instead, and keep Progress for the cases where the user reads their position rather than navigates it.
1 'use client' 2 import * as React from 'react' 3 import { Progress } from "@/components/ui/progress" 4 5 export default function CheckoutSteps() { 6 const [step, setStep] = React.useState(2) 7 const total = 5 8 9 return ( 10 <div className="space-y-3"> 11 <div className="flex justify-between text-sm"> 12 <span>Step {step} of {total}</span> 13 <button 14 className="text-muted-foreground hover:underline" 15 onClick={() => setStep((s) => Math.min(total, s + 1))} 16 > 17 Next → 18 </button> 19 </div> 20 <Progress value={step} max={total} /> 21 </div> 22 ) 23 }
Page-scroll progress indicator
A scroll-progress bar at the top of a long article tells readers how far they are through the content — a common pattern on blog posts, documentation pages, and long-form essays. Listen to the scroll event on window, compute the percentage as scrollY / (documentHeight - viewportHeight), and pass the clamped result to <Progress />. The Drivn component clamps to [0, 100] automatically, so the scroll math does not need to guard against the value exceeding 100 when the user over-scrolls past the document end on a touch device.
Position the bar with fixed top-0 left-0 right-0 z-50 to pin it across the top of the viewport, and reduce the track height for a thinner indicator by passing className="h-1" — the className flows through cn() and overrides the default h-2. The 300-millisecond bar transition feels slow at scroll cadence, so for a tighter scroll feel pass className="h-1 [&>div]:transition-none" (or fork the registry source to drop the bar transition entirely). The pattern pairs well with a fixed Drivn Header sitting just above it.
1 'use client' 2 import * as React from 'react' 3 import { Progress } from "@/components/ui/progress" 4 5 export default function ScrollProgress() { 6 const [pct, setPct] = React.useState(0) 7 8 React.useEffect(() => { 9 const onScroll = () => { 10 const h = document.documentElement 11 const max = h.scrollHeight - h.clientHeight 12 setPct(max > 0 ? (h.scrollTop / max) * 100 : 0) 13 } 14 onScroll() 15 window.addEventListener('scroll', onScroll, { passive: true }) 16 return () => window.removeEventListener('scroll', onScroll) 17 }, []) 18 19 return ( 20 <Progress 21 value={pct} 22 className="fixed top-0 left-0 right-0 z-50 h-1 rounded-none" 23 /> 24 ) 25 }
Password strength meter
A password-strength meter ties the percentage to a rule set rather than an external counter. Compute the score from the password contents — length, presence of digits, presence of symbols, mixed case — and map the score onto the [0, 100] range. The example below scores out of five rules (four character classes plus length ≥ 12) and renders the bar at twenty percent per satisfied rule. Pair the bar with a short label that names the strength tier ("Weak", "Fair", "Strong") so the user gets a categorical read in addition to the percentage.
For color tiers (red below 40 percent, amber 40-70, green above 70), override the bar color through a Tailwind arbitrary-value selector: className="[&>div]:bg-destructive" for the red tier, [&>div]:bg-warning for amber, and the default bg-primary for green. The bar transitions smoothly between colors because the transition-all class on the inner <div> covers the background-color transition out of the box. For form-level validation that drives the percentage, pair the meter with the Drivn Input and react-hook-form plus zod — the same zod schema can score the password and gate the submit button.
1 'use client' 2 import * as React from 'react' 3 import { Progress } from "@/components/ui/progress" 4 import { Input } from "@/components/ui/input" 5 6 function scorePassword(pw: string) { 7 let score = 0 8 if (pw.length >= 12) score++ 9 if (/[a-z]/.test(pw)) score++ 10 if (/[A-Z]/.test(pw)) score++ 11 if (/\d/.test(pw)) score++ 12 if (/[^A-Za-z0-9]/.test(pw)) score++ 13 return (score / 5) * 100 14 } 15 16 export default function PasswordStrength() { 17 const [pw, setPw] = React.useState('') 18 const pct = scorePassword(pw) 19 20 return ( 21 <div className="space-y-2"> 22 <Input 23 type="password" 24 value={pw} 25 onChange={(e) => setPw(e.target.value)} 26 placeholder="Choose a password" 27 /> 28 <Progress value={pct} /> 29 <p className="text-sm text-muted-foreground"> 30 {pct < 40 ? 'Weak' : pct < 80 ? 'Fair' : 'Strong'} 31 </p> 32 </div> 33 ) 34 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The registry source does not use React.useState, React.useEffect, or any other client-only API — the percentage is computed inline during render and written into the inner <div> width via the style prop. That means the component can sit inside a Server Component and render at request time without forcing the surrounding tree onto the client. The call site needs the directive only when it owns interactive state itself (the upload bar and password meter examples both add 'use client' because the surrounding component runs useState).
The registry source assumes value is always a known number, so there is no built-in indeterminate state. Adding one is a small edit: accept value: number | null, branch on value === null in the body, and render the bar with a looping animate-pulse class instead of the inline width style. Drop the aria-valuenow attribute when the state is indeterminate so screen readers announce the bar as "Progress, busy" rather than "Progress, zero percent". The patch is about ten lines and keeps the file under fifty lines total.
Yes, through Tailwind's arbitrary-variant selector. Pass className="[&>div]:bg-destructive" to color the bar red for the call site without forking the registry. The selector targets the inner <div> (the bar) and overrides its bg-primary default. For tiered coloring (red below 40, amber 40-70, green above 70), compute the className alongside the percentage and pass the matching value. The selector pattern keeps the registry source intact and confines the override to the one call site that needs it.
The component clamps the computed percentage with Math.min(100, Math.max(0, (value / max) * 100)) before writing it to the bar width. A value of 150 with the default max of 100 caps the bar at 100 percent — the width never exceeds the track. A negative value caps at 0 percent. The clamp lives in the registry source rather than inside a Radix internal, so any edit to the percentage math is visible in the component file.
The bar ships with transition-all duration-300 ease-out on the inner <div>. For a faster transition, override it through the className prop with a [&>div]:duration-100 selector. For an instant update (no transition), use [&>div]:transition-none. For maximum smoothness on a very high-frequency value (an audio scrubber updating every animation frame), fork the registry source to swap the inline width for transform: scaleX(${pct / 100}) with transformOrigin: left — the transform path composites on the GPU rather than triggering a layout. See Drivn vs shadcn Progress for the width-vs-transform trade-off discussion.