← Back to gallery
CSS

Native <progress> Variants

Three skinned fills on top of the native <progress> element — a canonical two-stop gradient, a six-stop rainbow, and a diagonal repeating-stripe overlay that scrolls. Native element stays in the DOM so screen readers announce the value.

progress-bardeterminatelinear-gradientrepeating-linear-gradientappearance-resetaria-labelprefers-reduced-motion

Progress / native / fill treatments

Native <progress> variants

Three skinned progress fills on top of the native <progress> element — a canonical two-color gradient, a multi-stop rainbow, and a diagonal stripe overlay. Stage cards run a full fill sweep on a synced cycle so the differences in decoration read clearly; inspector lets you pull the value slider yourself.

cyan → violet · canonical look

Gradient Fill

A two-stop linear gradient running cyan → violet — matches the gallery thumbnail. The simplest decoration: one fill, one direction, two hues.

  • linear-gradient
  • two-stop
  • canonical

six-stop spectrum · vivid scale

Rainbow Fill

A six-stop rainbow gradient (red → orange → yellow → green → cyan → violet). The vivid spectrum reads as a celebratory or "complete me" indicator — louder than the canonical gradient.

  • multi-stop
  • rainbow
  • high contrast

repeating-linear-gradient · barber pole

Striped Fill

Diagonal repeating stripes layered over the fill. A slow translation keeps the stripes moving so the bar reads as actively working even when the value holds.

  • repeating gradient
  • barber pole
  • active progress

Skin inspector

Gradient Fill

Deploy progress
  • linear-gradient
  • two-stop
  • canonical

A two-stop linear gradient running cyan → violet — matches the gallery thumbnail. The simplest decoration: one fill, one direction, two hues. Use when the surrounding UI is restrained and the bar should read as a calm progress indicator rather than a vivid one.

Helped you ship something? 🐟 Send my cat a churu

.progress-bar {
  appearance: none;
  -webkit-appearance: none;
  height: 12px;
  background: rgba(148, 163, 184, 0.22);
  border-radius: 999px;
  overflow: hidden;
}

.progress-bar::-webkit-progress-value {
  background: linear-gradient(90deg, rgba(103, 232, 249, 0.92), rgba(167, 139, 250, 0.95));
}

.progress-skin__fill {
  width: 60%;
  background: linear-gradient(90deg, rgba(103, 232, 249, 0.92), rgba(167, 139, 250, 0.95));
}

How to make this

A native progress pattern keeps the real <progress> element for semantics, then paints a separate aria-hidden skin for portable visual styling.

html
1<label class="native-progress-demo">  <span>Upload progress</span>3  <progress value="64" max="100">64%</progress>4  <span class="native-progress-demo__skin" aria-hidden="true">    <span class="native-progress-demo__fill"></span>  </span></label> <style>.native-progress-demo {  display: grid;  gap: .5rem;  width: min(100%, 22rem);  color: #dbeafe;  font: 600 .95rem/1.2 ui-sans-serif, system-ui;}17.native-progress-demo progress {  position: absolute;  inline-size: 1px;  block-size: 1px;  overflow: hidden;  clip: rect(0 0 0 0);}.native-progress-demo__skin {  position: relative;  overflow: hidden;  block-size: .85rem;  border-radius: 999px;  background: rgba(148, 163, 184, .24);}.native-progress-demo__fill {  display: block;  block-size: 100%;34  inline-size: 64%;  border-radius: inherit;  background:    repeating-linear-gradient(-45deg,      rgba(255,255,255,.22) 0 8px,      transparent 8px 16px),    linear-gradient(90deg, #67e8f9, #a78bfa);41  animation: native-progress-recipe-stripes 1.4s linear infinite;}@keyframes native-progress-recipe-stripes {  to { background-position: 32px 0, 0 0; }}@media (prefers-reduced-motion: reduce) {  .native-progress-demo__fill { animation: none; }}</style>

Annotated snippet

  1. Line 1The label connects the visible text to the native progress element. This keeps the value understandable even though the painted skin is decorative.
    PitfallWhy keep a native <progress> if I paint a custom bar?

    The native element gives assistive technology the value, max, and progress semantics for free. A decorative skin can handle branding while the real progress element remains the accessible source of truth.

  2. Line 3Keep a real <progress> with value and max. Screen readers can announce the determinate percentage without you rebuilding progressbar semantics by hand.
    PitfallWhy keep a native <progress> if I paint a custom bar?

    The native element gives assistive technology the value, max, and progress semantics for free. A decorative skin can handle branding while the real progress element remains the accessible source of truth.

  3. Line 4The skin is aria-hidden because it duplicates the native value visually. It can be as decorative as needed without changing the accessible object.
  4. Line 17The native element is visually hidden, not removed. Avoid display: none, which removes the semantic progress value from assistive technology.
    PitfallCan I use display: none on the progress element?

    No. display: none removes it from the accessibility tree. Visually hide it with clipping or a screen-reader-only utility, then put any custom painted skin in an aria-hidden sibling.

  5. Line 34The fill width mirrors value / max. In production, keep this value synchronized with the progress element so visual and semantic states never disagree.
    PitfallHow do I keep native progress styling consistent across browsers?

    Direct pseudo-element styling differs between WebKit and Firefox. A hidden native element plus custom skin is more portable. If styling the element directly, test ::-webkit-progress-value and ::-moz-progress-bar separately.

  6. Line 41Animated stripes imply active work while the numeric value is holding. They are decoration, so reduced motion can stop them without hiding progress.

    Same 64% fill width on both. A solid fill freezes — users cannot tell if the upload is still working or has stalled. Animated diagonal stripes on top of the same fill make the bar feel actively progressing.

    PitfallWhat should reduced motion do for progress bars?

    Stop decorative stripe or sweep animations while keeping the current fill and accessible progress value visible. Reduced motion should not make an in-progress task look complete or absent.

Other pitfalls

Are animated striped progress bars expensive?
Small background-position stripes are usually fine, but many animated bars can add paint work. Use them only for active tasks and stop the animation when the task completes.

Notes

Overview

The native <progress> element ships with ARIA semantics, screen-reader value announcement, and determinate / indeterminate states built in. The pattern paints custom skins on top of the native element via appearance: none + a sibling overlay, keeping the native element in the DOM as the accessibility source of truth.

When to use it

Reach for native <progress> on file uploads, multi-step wizards, video / audio scrubbers, and any determinate progress that should announce to AT. Skip the native element when the progress is meaningless (e.g. decorative loading) — a CSS-only spinner is lighter. Always keep the native element with aria-label describing what the progress measures.

How it works

Reset native styling with progress { appearance: none; -webkit-appearance: none } and target the internal pseudo-elements: progress::-webkit-progress-bar (the container) and progress::-webkit-progress-value (the filled portion) for Chromium/Safari; Firefox uses progress::-moz-progress-bar. Add a transition on the inner value so updates to the value attribute animate smoothly. For indeterminate state (omitting the value attribute), Chromium and Safari paint their own marching-ants animation by default; Firefox does not, so add a CSS ::-moz-progress-bar animation as a fallback.

Production gotchas

Pseudo-element selectors are vendor-specific and non-standard — the four-rule cross-browser stack is verbose but required. Some screen readers ignore custom styles entirely and announce the native value, which is exactly what you want; resist the urge to add your own ARIA value announcement on top or you double-announce. Animating the value too aggressively can confuse assistive tech; throttle updates to at most ~10Hz so announcements stay coherent.

Accessibility

The native element handles aria-valuenow, aria-valuemin, and aria-valuemax automatically when you set the value + max HTML attributes — nothing custom required. Add an aria-label describing the measured operation (“Upload progress” not “Progress bar”). Under prefers-reduced-motion: reduce drop the smooth-value transition so values snap to position.

References

Implementation depth

The native progress element already carries value semantics, so keep it in the DOM and style the platform parts around it. The visual fill should reflect the actual value attribute.

Avoid turning determinate progress into decorative motion. Repeating stripes can imply activity, but screen readers need the current value and reduced motion should stop stripe travel while preserving the fill length.