Skip to content

Progress Bar

Indicates the completion progress of a task or operation.

  • Loading Bar
  • Progress Indicator

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

VariantVisual TreatmentUse Case
LinearHorizontal bar, left-to-right fill. The classic progress bar.File uploads, form completion, quota meters
Circular / RadialRing or arc that fills clockwise.Dashboard widgets, profile completion, compact spaces
Semicircular / GaugeHalf-circle arc, often with a needle or fill.Performance scores, speed indicators, health metrics
SegmentedBar divided into discrete steps.Multi-step processes where each step is a distinct phase
Stacked / Multi-partMultiple colored segments within one bar.Storage breakdowns (photos, videos, documents), budget allocation
Thin / Slim2–4px bar, typically at the very top of the page.Page load indicators (YouTube, GitHub), background operations

Semantic Variants

VariantColorPurpose
DefaultBrand blue or neutralStandard operations with no semantic meaning
SuccessGreenCompletion state or healthy quota levels
WarningAmber/YellowApproaching limits (80%+ of quota)
DangerRedExceeded limits or failed operations

Size Variants

SizeHeightLabel PlacementUse Case
xs2–4pxExternal or nonePage-level loading bars, minimal footprint
sm6–8pxExternal onlyInline progress in lists or cards
md12–16pxInternal or externalDefault for most use cases
lg20–24pxInternal (label inside bar)Hero progress displays, dashboards

Animation Variants

  • Determinate: Smooth transition from current value to new value using CSS transition or 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 @keyframes with translateX for 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

PropertyTypeDefaultDescription
valuenumber0Current progress value (0–100, or custom range). Required for determinate bars.
maxnumber100Maximum value for the progress range
minnumber0Minimum value (usually 0)
indeterminatebooleanfalseWhen 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
labelstring | ReactNode—Text displayed near or inside the bar
showValuebooleanfalseDisplays the current percentage or value text
valueFormat(value: number, max: number) => string—Custom formatter for displayed value (e.g., "3 of 5 steps")
stripedbooleanfalseApplies animated diagonal stripes to the fill
animatedbooleantrueWhether the fill width transitions smoothly
colorstring—Custom fill color, overriding the variant. Generate palettes with the Color Palette Generator.
trackColorstring—Custom track (background) color
borderRadiusstring | number—Override border-radius. Preview with the Border Radius Generator.
aria-labelstring—Accessible label when no visible label is present. Required for icon-only or unlabeled bars.
aria-labelledbystring—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 CategoryToken ExampleProgress Bar Usage
Color – Fill--color-primary-500Default fill color
Color – Success--color-success-500Success variant fill
Color – Warning--color-warning-500Warning variant fill
Color – Danger--color-error-500Danger variant fill
Color – Track--color-neutral-200Track (unfilled) background
Color – Label--color-on-primaryLabel 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-4Bar height per size variant
Spacing – Gap--space-2Gap between bar and external label
Transition--duration-normal (300ms), --ease-outFill width animation. Preview with Animation & Easing Tool.
Shadow--shadow-inner-smOptional 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

StateVisual BehaviorImplementation
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 ProgressFill 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.
IndeterminateLooping shimmer or sliding gradient animation. No numeric value shown.Set indeterminate={true}. Use CSS @keyframes for the animation. Announce with aria-busy="true".
PausedFill 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 / FailedFill turns red, may show an error icon or message.Switch variant to danger. Provide error context via label or adjacent Alert.
BufferingTwo-layer fill: solid for actual progress, translucent for buffered amount.Render two <div> fills inside the track, with the buffer fill at lower opacity.
OverflowValue 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

CriterionLevelRequirement for Progress Bars
1.3.1 Info and Relationships (A)AUse <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)ADon'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)AAFill-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)AAThe 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)AIf 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)AScreen readers must be able to determine the component's name (label), role (progressbar), and current value.
4.1.3 Status Messages (AA)AAProgress 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

html
<!-- 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>
tsx
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.

FeedbackLoading BarProgress Indicator