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.