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