CSS Progress Bar Generator
Determinate, indeterminate, spinner and dots — with the right ARIA.
.progress {
overflow: hidden;
height: 8px;
border-radius: 999px;
background: #e4e4e7;
}
.progress__fill {
height: 100%;
border-radius: 999px;
background: #2563eb;
width: 40%;
animation: progress-slide 1.4s ease-in-out infinite;
}
@keyframes progress-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
@media (prefers-reduced-motion: reduce) {
.progress__fill {
animation: none;
width: 100%;
background: rgb(37 99 235 / 0.4);
}
}
Indeterminate is an ARIA state, not just an animation
An indeterminate progress bar is two things at once: an animation that loops, and a promise to assistive technology that you do not know how long this will take. Most snippets get the first right and skip the second entirely.
How to use it
-
Pick a type
Determinate for known progress, indeterminate for unknown, or a spinner or dots when there is no bar to fill.
-
Shape it
Height, radius, stripes and speed. The preview animates at the exact duration the CSS will contain.
-
Set the colours
Fill and track. Keep enough contrast between them that the bar is visible at small heights.
-
Copy both files
The CSS includes a prefers-reduced-motion fallback, and the HTML has the right ARIA for the type you chose.
The ARIA half
A role="progressbar" with no aria-valuenow is what makes progress
indeterminate to a screen reader. Supplying a placeholder value — 0, or 100 — states something
false. The HTML output here omits the attribute deliberately, with a comment saying why, so
nobody helpfully adds it back.
Animate transform, never width
The classic indeterminate bar is a fixed-width fill translated across a clipped track.
Animating transform keeps the work on the compositor; animating
width or left forces layout on every frame — and a loading indicator
runs precisely when the main thread is already busy, which is when that cost shows.
Indeterminate beats a fake percentage
If you cannot compute real progress, do not invent it. A bar that races to 90% and stalls is worse than an honest looping one, because it makes a prediction the user then watches fail.
Reduced motion is not optional here
Loading animations loop indefinitely, which makes them one of the worst offenders for people
with vestibular disorders. Every export includes a
prefers-reduced-motion block that stops the loop while keeping the loading state
visible — see the animation generator for the general
case.
Frequently asked questions
How do I make an indeterminate progress bar in CSS?
transform: translateX() across the track on an infinite loop, with the track set to overflow: hidden. Animating transform rather than width or left keeps it on the compositor, so it holds 60fps while the page is busy loading.What makes a progress bar indeterminate to a screen reader?
aria-valuenow. A role="progressbar" with no current value is the standard way to say "in progress, length unknown". Adding a fake value like 0 or 100 tells assistive technology something untrue.When should I use indeterminate instead of a percentage?
How do I make a CSS-only loading spinner?
border-radius: 50%, a border on all four sides, one side coloured differently, and animation: spin 1s linear infinite rotating it 360 degrees. No SVG, no image, no JavaScript.Why is my progress animation janky?
width, left or margin, all of which force layout on every frame. Animate transform instead — it is handled by the compositor and does not trigger layout or paint.Do loading animations need a reduced-motion fallback?
prefers-reduced-motion block that stops or slows the loop while keeping the loading state visible.