Overview
The Progress Bar (also called a loading bar, progress indicator, or meter) is a feedback component that visually communicates the completion status of a task or operation. It answers the most fundamental question a user has during any wait: "How much longer?"
Progress bars reduce perceived wait time by giving users a sense of forward momentum. Research by Jakob Nielsen and others consistently shows that providing progress feedback — even approximate — makes users perceive waits as 10–20% shorter than identical waits with no indicator. This isn't just UX polish; it's a measurable reduction in abandonment rates for file uploads, multi-step forms, and data processing workflows.
When to use a Progress Bar:
- File uploads or downloads where you know the total size
- Multi-step wizards or onboarding flows (combined with a Stepper)
- Data processing operations where percentage is calculable
- Page load indicators (e.g., YouTube's top-of-page bar)
- Quota or capacity displays ("75% of storage used")
- Skill or profile completion meters
When NOT to use a Progress Bar:
- When you can't calculate progress — use a Spinner or Skeleton instead
- For very short operations (< 1 second) — the bar will flash and feel janky
- For real-time streaming data — use a live counter or Stat
- For binary loading states with no progress info — use a Spinner
Determinate vs. Indeterminate: A progress bar can be determinate (0%→100% with known progress) or indeterminate (animated loop indicating "something is happening" without quantifying it). If you find yourself using an indeterminate progress bar, ask whether a Spinner would be more appropriate — indeterminate bars are most useful when the operation will eventually become determinate (e.g., waiting for a server to calculate the total before showing real progress).
Use our Color Palette Generator to pick semantically meaningful fill colors (blue for neutral progress, green for completion, red for quota warnings). Preview your bar's animation easing with the Animation & Easing Tool. Verify label contrast with the Contrast Checker.
Variants
Visual Variants
| Variant | Visual Treatment | Use Case |
|---|---|---|
| Linear | Horizontal bar, left-to-right fill. The classic progress bar. | File uploads, form completion, quota meters |
| Circular / Radial | Ring or arc that fills clockwise. | Dashboard widgets, profile completion, compact spaces |
| Semicircular / Gauge | Half-circle arc, often with a needle or fill. | Performance scores, speed indicators, health metrics |
| Segmented | Bar divided into discrete steps. | Multi-step processes where each step is a distinct phase |
| Stacked / Multi-part | Multiple colored segments within one bar. | Storage breakdowns (photos, videos, documents), budget allocation |
| Thin / Slim | 2–4px bar, typically at the very top of the page. | Page load indicators (YouTube, GitHub), background operations |
Semantic Variants
| Variant | Color | Purpose |
|---|---|---|
| Default | Brand blue or neutral | Standard operations with no semantic meaning |
| Success | Green | Completion state or healthy quota levels |
| Warning | Amber/Yellow | Approaching limits (80%+ of quota) |
| Danger | Red | Exceeded limits or failed operations |
Size Variants
| Size | Height | Label Placement | Use Case |
|---|---|---|---|
| xs | 2–4px | External or none | Page-level loading bars, minimal footprint |
| sm | 6–8px | External only | Inline progress in lists or cards |
| md | 12–16px | Internal or external | Default for most use cases |
| lg | 20–24px | Internal (label inside bar) | Hero progress displays, dashboards |
Animation Variants
- Determinate: Smooth transition from current value to new value using CSS
transitionor a spring animation. Preview easing curves with the Animation & Easing Tool. - Indeterminate: A looping animation — typically a shimmer, pulse, or sliding gradient that moves left-to-right continuously. Use
@keyframeswithtranslateXfor performance (GPU-accelerated). - Buffer: Two layers — a primary fill showing actual progress and a secondary (lighter) fill showing buffered/pre-loaded progress. Common in video players.
- Striped: Diagonal stripes animated across the fill, indicating active processing. Popular in Bootstrap-style systems.
Properties
Progress Bar Properties
| Property | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Current progress value (0–100, or custom range). Required for determinate bars. |
max | number | 100 | Maximum value for the progress range |
min | number | 0 | Minimum value (usually 0) |
indeterminate | boolean | false | When true, displays a looping animation with no specific value |
variant | 'default' | 'success' | 'warning' | 'danger' | 'default' | Semantic color variant |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Controls bar height |
label | string | ReactNode | — | Text displayed near or inside the bar |
showValue | boolean | false | Displays the current percentage or value text |
valueFormat | (value: number, max: number) => string | — | Custom formatter for displayed value (e.g., "3 of 5 steps") |
striped | boolean | false | Applies animated diagonal stripes to the fill |
animated | boolean | true | Whether the fill width transitions smoothly |
color | string | — | Custom fill color, overriding the variant. Generate palettes with the Color Palette Generator. |
trackColor | string | — | Custom track (background) color |
borderRadius | string | number | — | Override border-radius. Preview with the Border Radius Generator. |
aria-label | string | — | Accessible label when no visible label is present. Required for icon-only or unlabeled bars. |
aria-labelledby | string | — | ID of an element that labels this progress bar |
Important: For determinate progress bars, always provide a meaningful value. Screen readers announce progress changes; jumping from 0% to 100% instantly defeats the purpose. Update in reasonable increments (1–5%) for smooth, informative feedback.
Token Mappings
Design Token Mappings
Progress bars are deceptively token-rich — they touch color, spacing, motion, and border-radius. For a deep understanding of token architecture, see our Design Tokens Complete Guide.
| Token Category | Token Example | Progress Bar Usage |
|---|---|---|
| Color – Fill | --color-primary-500 | Default fill color |
| Color – Success | --color-success-500 | Success variant fill |
| Color – Warning | --color-warning-500 | Warning variant fill |
| Color – Danger | --color-error-500 | Danger variant fill |
| Color – Track | --color-neutral-200 | Track (unfilled) background |
| Color – Label | --color-on-primary | Label text inside filled region |
| Border Radius | --radius-full (9999px) | Pill-shaped bar ends. Preview with Border Radius Generator. |
| Spacing – Height | --size-2 / --size-3 / --size-4 | Bar height per size variant |
| Spacing – Gap | --space-2 | Gap between bar and external label |
| Transition | --duration-normal (300ms), --ease-out | Fill width animation. Preview with Animation & Easing Tool. |
| Shadow | --shadow-inner-sm | Optional inset shadow on the track for depth |
Dynamic Color Tokens
Many progress bars shift color as they approach thresholds. Map these to semantic tokens:
/* Auto-color based on value */
--progress-fill: var(--color-primary-500); /* 0–69% */
--progress-fill-warning: var(--color-warning-500); /* 70–89% */
--progress-fill-danger: var(--color-error-500); /* 90–100% */
This pattern is common for storage quotas, bandwidth meters, and capacity indicators. Generate all three scales from a single hue using the Color Palette Generator.
States
Progress Bar States
| State | Visual Behavior | Implementation |
|---|---|---|
| Empty (0%) | Track only, no fill visible. Label may show "0%" or "Not started". | value={0} — ensure the track itself is visible via background color or border. |
| In Progress | Fill width proportional to value. Smooth CSS transition between updates. | Update value in increments. Use transition: width 300ms ease-out for smooth animation. |
| Complete (100%) | Full fill, often transitions to success color. May show a checkmark icon. | Switch variant to success or show a completion message. Consider auto-hiding after a delay. |
| Indeterminate | Looping shimmer or sliding gradient animation. No numeric value shown. | Set indeterminate={true}. Use CSS @keyframes for the animation. Announce with aria-busy="true". |
| Paused | Fill stops at current position. Striped animation freezes. Visual dimming optional. | Remove animated class or set animation-play-state: paused. Update aria-label to include "paused". |
| Error / Failed | Fill turns red, may show an error icon or message. | Switch variant to danger. Provide error context via label or adjacent Alert. |
| Buffering | Two-layer fill: solid for actual progress, translucent for buffered amount. | Render two <div> fills inside the track, with the buffer fill at lower opacity. |
| Overflow | Value exceeds max (e.g., 120% of quota). Fill is fully red, label shows excess. | Clamp visual fill to 100% but display actual value in the label. Use danger variant. |
Accessibility
Accessibility — Progress Bar
Progress bars are straightforward to make accessible, but there are important nuances around live announcements and labeling. Validate your color choices with the Contrast Checker.
ARIA Pattern
Use the native <progress> element when possible — it provides built-in semantics. For custom-styled bars, use role="progressbar":
<!-- Native HTML -->
<label for="file-upload">Uploading document.pdf</label>
<progress id="file-upload" value="65" max="100">65%</progress>
<!-- Custom ARIA -->
<div role="progressbar"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Uploading document.pdf">
<div class="progress-fill" style="width: 65%"></div>
</div>
WCAG Success Criteria
| Criterion | Level | Requirement for Progress Bars |
|---|---|---|
| 1.3.1 Info and Relationships (A) | A | Use <progress> or role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax. The semantic structure must convey that this is a progress indicator. |
| 1.4.1 Use of Color (A) | A | Don't rely solely on color to indicate completion/warning/error states. Include text labels, percentages, or icons alongside color changes. |
| 1.4.3 Contrast (Minimum) (AA) | AA | Fill-to-track contrast must meet 3:1 for non-text UI components. Label text must meet 4.5:1 against its background. Use the Contrast Checker. |
| 1.4.11 Non-text Contrast (AA) | AA | The progress fill must have at least 3:1 contrast against the track. This applies to all color variants, including warning (yellow on light gray can fail). |
| 2.2.1 Timing Adjustable (A) | A | If the progress bar is tied to a time limit (e.g., session expiry countdown), provide a way to extend or disable the limit. |
| 4.1.2 Name, Role, Value (A) | A | Screen readers must be able to determine the component's name (label), role (progressbar), and current value. |
| 4.1.3 Status Messages (AA) | AA | Progress updates should be announced to assistive tech. Use aria-live="polite" on a container or rely on screen reader's native progressbar polling. |
Live Announcements
Screen readers poll role="progressbar" periodically, but the frequency varies. For critical progress (file uploads, form submissions), supplement with an aria-live region:
<div aria-live="polite" class="sr-only">
Upload progress: 65% complete
</div>
Don't announce every 1% increment — this floods the screen reader. Announce at meaningful intervals: 25%, 50%, 75%, 100%, and on error.
Indeterminate Accessibility
For indeterminate progress bars, omit aria-valuenow (don't set it to 0, which implies "no progress"). Set aria-busy="true" on the content region being loaded, and provide a text label like "Loading…":
<div role="progressbar" aria-label="Loading data…">
<!-- animated indeterminate bar -->
</div>
<div aria-busy="true">
<!-- content area being loaded -->
</div>
Usage Guidelines
Do
- Always show determinate progress when you can. If you know the total, show percentage. Users strongly prefer knowing "65% done" over an indefinite spinner.
- Use smooth transitions between value updates — jumping from 30% to 60% instantly feels broken. Apply
transition: width 300ms ease-out. Preview with the Animation & Easing Tool. - Label your progress bars. Even a simple "Uploading… 65%" dramatically improves usability. Place labels above, below, or inside the bar depending on size.
- Provide time estimates when possible. "Uploading… 65% — about 2 minutes remaining" is the gold standard.
- Use semantic colors for thresholds. Storage at 50%? Blue. At 85%? Amber. At 95%? Red. Generate consistent color scales with the Color Palette Generator.
- Consider the thin page-top bar for route changes in SPAs — it's a well-established pattern (GitHub, YouTube) that doesn't take up layout space.
Don't
- Don't fake progress. If you simulate progress that doesn't correlate with actual work, users will notice when it stalls at 99%. It destroys trust.
- Don't show progress bars for operations under 1 second. The bar will flash and disappear, which is more distracting than helpful.
- Don't use a progress bar when you have no progress data. Use a Spinner or Skeleton for indeterminate loading.
- Don't restart progress from 0% unless the operation has genuinely restarted. Backward movement feels like regression.
- Don't hide the bar while it's in progress. If the user navigates away, either continue in the background with a notification or confirm they want to cancel.
- Don't rely only on color for state changes — supplement with labels or icons for colorblind users (WCAG 1.4.1).
Content Guidelines
- Label text should describe what's happening: "Uploading document.pdf" not just "Loading"
- Percentage text should be right-aligned or inside the bar
- For multi-step processes, show "Step 3 of 5" alongside the bar
- Error states should describe what went wrong and offer a retry action
Code Snippets
<!-- Determinate Progress Bar -->
<div class="progress-bar-container">
<div class="progress-bar-header">
<span class="progress-bar-label">Uploading document.pdf</span>
<span class="progress-bar-value">65%</span>
</div>
<div class="progress-bar-track" role="progressbar"
aria-valuenow="65" aria-valuemin="0" aria-valuemax="100"
aria-label="Uploading document.pdf">
<div class="progress-bar-fill" style="width: 65%"></div>
</div>
</div>
<!-- Indeterminate Progress Bar -->
<div class="progress-bar-track progress-bar--indeterminate"
role="progressbar" aria-label="Loading data…">
<div class="progress-bar-fill"></div>
</div>
<!-- Slim Page-Top Progress Bar -->
<div class="progress-bar-top" role="progressbar"
aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"
aria-label="Page loading">
<div class="progress-bar-top-fill" style="width: 40%"></div>
</div>
<style>
.progress-bar-track {
width: 100%;
height: 12px;
background: var(--color-neutral-200);
border-radius: 9999px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--color-primary-500);
border-radius: 9999px;
transition: width 300ms ease-out;
}
.progress-bar--indeterminate .progress-bar-fill {
width: 40%;
animation: indeterminate 1.5s ease-in-out infinite;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
.progress-bar-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 14px;
}
.progress-bar-top {
position: fixed;
top: 0; left: 0; right: 0;
height: 3px;
z-index: 9999;
background: transparent;
}
.progress-bar-top-fill {
height: 100%;
background: var(--color-primary-500);
transition: width 200ms ease-out;
}
</style>import { forwardRef } from "react";
interface ProgressBarProps {
value?: number;
max?: number;
indeterminate?: boolean;
variant?: "default" | "success" | "warning" | "danger";
size?: "xs" | "sm" | "md" | "lg";
label?: string;
showValue?: boolean;
striped?: boolean;
className?: string;
}
const sizeMap = { xs: 4, sm: 8, md: 12, lg: 20 };
const variantColors = {
default: "var(--color-primary-500)",
success: "var(--color-success-500)",
warning: "var(--color-warning-500)",
danger: "var(--color-error-500)",
};
export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
(
{
value = 0,
max = 100,
indeterminate = false,
variant = "default",
size = "md",
label,
showValue = false,
striped = false,
className,
...props
},
ref
) => {
const pct = Math.min(100, Math.max(0, (value / max) * 100));
return (
<div className={className} ref={ref}>
{(label || showValue) && (
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 4, fontSize: 14 }}>
{label && <span>{label}</span>}
{showValue && !indeterminate && <span>{Math.round(pct)}%</span>}
</div>
)}
<div
role="progressbar"
aria-valuenow={indeterminate ? undefined : value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={label ?? "Progress"}
style={{
width: "100%",
height: sizeMap[size],
background: "var(--color-neutral-200)",
borderRadius: 9999,
overflow: "hidden",
}}
{...props}
>
<div
style={{
height: "100%",
width: indeterminate ? "40%" : pct + "%",
background: variantColors[variant],
borderRadius: 9999,
transition: indeterminate ? "none" : "width 300ms ease-out",
animation: indeterminate ? "indeterminate 1.5s ease-in-out infinite" : "none",
}}
/>
</div>
</div>
);
}
);
ProgressBar.displayName = "ProgressBar";Design Systems
Design System Implementations
Material Design 3 offers both linear and circular progress indicators with determinate and indeterminate variants. The linear variant uses a thin 4px track by default, with the indeterminate animation consisting of two bars that grow and shrink independently — a nuanced animation that feels more natural than a simple slide. Material also provides a "buffer" variant with a secondary track for pre-loading states (common in video players). The circular variant is their spinner equivalent, doubling as both progress indicator and loading spinner depending on whether you provide a value.
Ant Design provides a Progress component with line, circle, and dashboard (semicircle) types. It supports steps for segmented progress, strokeColor for custom gradients (pass an object with from and to), and a format prop for custom value display. Ant's success prop lets you define a sub-section within the bar that's colored green — useful for showing "completed" vs "in progress" portions separately.
Chakra UI's Progress component supports hasStripe and isAnimated props for the classic Bootstrap-style striped animation. It also supports the colorScheme prop to change the fill color and isIndeterminate for the looping animation. Chakra uses the native <div> approach with ARIA roles rather than the <progress> element, giving full styling control.
Bootstrap's progress bar is perhaps the most widely recognized implementation. It introduced the striped/animated pattern that many design systems have adopted. Bootstrap uses a .progress container with .progress-bar inner div, supports stacking multiple bars within one container for multi-part progress, and offers contextual classes (bg-success, bg-warning, bg-danger).
Shadcn/ui keeps it minimal: a track and fill with Tailwind classes. The component is a thin wrapper around a styled div. No built-in indeterminate, striped, or circular variants — compose those yourself.
Radix UI provides a Progress primitive with value and max props, along with ProgressIndicator as a child for the fill. It handles all ARIA attributes automatically and leaves styling entirely to you. It correctly omits aria-valuenow when value is null (indeterminate).
For choosing fill colors that maintain contrast across all variants and themes, use the Color Palette Generator. Preview animation easing with the Animation & Easing Tool.