Skip to content
Drivn
7 min read

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.

A progress bar is the right affordance whenever the UI needs to communicate "how far through a known total" — an upload moving from 0 to 100 percent, a multi-step form on step three of five, a reader scrolled 40 percent down a long article. The Progress component in Drivn renders a w-full h-2 bg-accent rounded-full overflow-hidden track with a bg-primary bar inside it whose width tracks a clamped percentage computed from value and max. The whole tree lives in @/components/ui/progress.tsx, runs forty lines of TypeScript, and imports nothing past react (for the type namespace) and the local cn() utility. There is no Radix dependency, no portal, and no 'use client' directive — the component renders cleanly inside a Server Component.

This page collects the call-site shapes that ship in real projects. Every example imports the component from @/components/ui/progress — the path the Drivn CLI installs under — and uses the same <Progress value={...} /> signature. Every snippet is TypeScript because Drivn ships TypeScript-only.

The examples below cover a default fixed-value bar, an animated upload progress bar that ticks in real time, a multi-step form indicator that maps step counters onto a custom max, a page-scroll progress indicator that listens to the window scroll event, and a password-strength meter that ties the percentage to a zod-validated rule set. The full component reference and props table live on the Progress docs page, and the shadcn/ui comparison sits on Drivn vs shadcn Progress. For a fresh install, see Installation and the Drivn CLI docs.

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.

1import { Progress } from "@/components/ui/progress"
2
3export 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'
2import * as React from 'react'
3import { Progress } from "@/components/ui/progress"
4
5export 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

For a multi-step form, pass the current step as value and the total step count as max. The component does the percentage math — step 2 of 5 renders as 40 percent, step 4 of 5 as 80 percent. The pattern is cleaner than computing the percentage at the call site because the max prop documents the total count in one place: bumping the wizard from five to six steps means changing the max in one component, not in the percentage formula on every page. The same shape works for completed-tasks-out-of-total indicators, lessons-out-of-curriculum bars, and any "step N of M" UI.

Pair the bar with a short label above it (Step 2 of 5) so screen readers and sighted users get the same information. The label can read from the same state the bar reads from, which keeps the two in sync without prop drilling. For a clickable variant that lets the user jump between steps, swap the bar for a Drivn Stepper. The Stepper is the interactive sibling — Progress communicates the percentage, Stepper communicates the discrete step list and lets the user navigate it.

1'use client'
2import * as React from 'react'
3import { Progress } from "@/components/ui/progress"
4
5export 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'
2import * as React from 'react'
3import { Progress } from "@/components/ui/progress"
4
5export 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'
2import * as React from 'react'
3import { Progress } from "@/components/ui/progress"
4import { Input } from "@/components/ui/input"
5
6function 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
16export 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}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.