SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/progress-bars

8 CSS Progress Bars

7 Pure CSS1 with JSPublished 2026-10-04Free · no sign-up

Eight ways to show progress, from a styled native progress element to a circular ring, segmented steps, an indeterminate loading line, a fundraising goal, a stacked storage meter and a bar driven by JavaScript.

01. Labelled Progress Bar

ProgressAccessible

A simple determinate progress bar built on the native progress element, so screen readers announce the percentage. The bar is restyled for WebKit and Firefox with their own pseudo-elements, and the value is also printed as text above it.

View code
<div class="pb">
  <div class="pb-top"><label for="up">Uploading catalogue.csv</label><span>64%</span></div>
  <progress id="up" max="100" value="64">64%</progress>
</div>
.pb { width: min(380px, 90vw); font: 600 14px Inter, system-ui, sans-serif; color: #344054; }
.pb-top { display: flex; justify-content: space-between; margin-bottom: 8px; }
.pb-top span { color: #101828; font-variant-numeric: tabular-nums; }
.pb progress { appearance: none; width: 100%; height: 10px; border: 0; border-radius: 999px; background: #e4e7ec; overflow: hidden; }
.pb progress::-webkit-progress-bar { background: #e4e7ec; border-radius: 999px; }
.pb progress::-webkit-progress-value { background: #2563eb; border-radius: 999px; }
.pb progress::-moz-progress-bar { background: #2563eb; border-radius: 999px; }

02. Animated Striped Progress

AnimationStripes

A striped bar whose diagonal stripes move to show that work is happening. The stripes are a repeating linear gradient and the motion animates background-position, which is cheap to render; the movement stops when reduced motion is requested.

View code
<div class="sp" role="progressbar" aria-label="Exporting orders" aria-valuemin="0" aria-valuemax="100" aria-valuenow="45">
  <span style="width:45%"></span>
</div>
.sp { width: min(380px, 90vw); height: 14px; border-radius: 999px; background: #1e2433; overflow: hidden; }
.sp span { display: block; height: 100%; border-radius: inherit; background: repeating-linear-gradient(45deg, #f59e0b 0 10px, #fbbf24 10px 20px); background-size: 28px 28px; animation: sp-move 1s linear infinite; }
@keyframes sp-move { to { background-position: 28px 0; } }
@media (prefers-reduced-motion: reduce) { .sp span { animation: none; } }

03. Segmented Step Progress

StepsCheckout

Progress shown as a row of segments, one per step, which suits checkouts and onboarding. Completed segments are filled, the current one is half filled, and the text Step 3 of 5 gives the same information without relying on the bars.

View code
<div class="seg">
  <p>Step 3 of 5: Shipping</p>
  <ol aria-hidden="true"><li class="done"></li><li class="done"></li><li class="now"></li><li></li><li></li></ol>
</div>
.seg { width: min(380px, 90vw); font: 600 14px Inter, system-ui, sans-serif; color: #101828; }
.seg p { margin: 0 0 10px; }
.seg ol { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin: 0; padding: 0; list-style: none; }
.seg li { height: 8px; border-radius: 4px; background: #e4e7ec; }
.seg li.done { background: #16a34a; }
.seg li.now { background: linear-gradient(90deg, #16a34a 50%, #e4e7ec 50%); }

04. Circular Progress Ring

SVGRing

A circular progress ring drawn with two SVG circles. The coloured arc length comes from stroke-dasharray and stroke-dashoffset, set from a custom property so one number controls both the ring and the label in the middle.

View code
<div class="ring" role="progressbar" aria-label="Storage used" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" style="--p:72">
  <svg viewBox="0 0 120 120" aria-hidden="true"><circle cx="60" cy="60" r="52"/><circle cx="60" cy="60" r="52" class="arc"/></svg>
  <span><b>72%</b>used</span>
</div>
.ring { position: relative; width: 150px; height: 150px; font-family: Inter, system-ui, sans-serif; }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke: #262d3e; stroke-width: 12; }
.ring .arc { stroke: #22d3ee; stroke-linecap: round; stroke-dasharray: 326.7; stroke-dashoffset: calc(326.7 - 326.7 * var(--p) / 100); }
.ring span { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; color: #9aa4b8; font-size: 13px; }
.ring b { color: #fff; font-size: 28px; }

05. Indeterminate Loading Bar

LoadingAnimation

A thin bar for when you do not know how long something will take, like the loading line at the top of many apps. A coloured segment slides across repeatedly, the progress element has no value so it is announced as busy, and reduced motion shows a static bar.

View code
<div class="ind">
  <p id="ind-l">Checking your store connection</p>
  <progress aria-labelledby="ind-l"></progress>
</div>
.ind { width: min(380px, 90vw); font: 500 14px Inter, system-ui, sans-serif; color: #344054; }
.ind p { margin: 0 0 10px; }
.ind progress { appearance: none; position: relative; display: block; width: 100%; height: 4px; border: 0; border-radius: 2px; background: #dbeafe; overflow: hidden; }
.ind progress::-webkit-progress-bar { background: #dbeafe; }
.ind progress::after { content: ""; position: absolute; top: 0; bottom: 0; width: 35%; border-radius: 2px; background: #2563eb; animation: ind-slide 1.3s ease-in-out infinite; }
.ind progress::-moz-progress-bar { background: #2563eb; width: 35%; }
@keyframes ind-slide { from { left: -35%; } to { left: 100%; } }
@media (prefers-reduced-motion: reduce) { .ind progress::after { animation: none; left: 0; } }

06. Gradient Goal Progress

GradientFundraising

A goal tracker for fundraising or sales targets, with a gradient fill, the amount raised, the target and a marker for the halfway point. The numbers are written out in text, so the bar itself is decorative and hidden from screen readers.

View code
<div class="goal">
  <p><b>$7,450</b> raised of $10,000 goal</p>
  <div class="track" aria-hidden="true"><span style="width:74.5%"></span><i></i></div>
  <small>214 supporters, 9 days left</small>
</div>
.goal { width: min(380px, 90vw); font: 14px Inter, system-ui, sans-serif; color: #aab3c7; }
.goal p { margin: 0 0 10px; }
.goal b { color: #fff; font-size: 22px; }
.track { position: relative; height: 12px; border-radius: 999px; background: #1e2433; }
.track span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #a855f7, #ec4899, #f59e0b); }
.track i { position: absolute; left: 50%; top: -4px; bottom: -4px; width: 2px; background: #aab3c7; }
.goal small { display: block; margin-top: 10px; }

07. Stacked Usage Bar

DataLegend

One bar split into coloured parts, like a storage meter showing photos, video and documents. Each part's width is its share, and a legend below repeats every value in text so the breakdown is readable without telling the colours apart.

View code
<div class="stack">
  <p>42 GB of 64 GB used</p>
  <div class="bar" aria-hidden="true"><span style="width:30%;background:#2563eb"></span><span style="width:22%;background:#f59e0b"></span><span style="width:13.6%;background:#16a34a"></span></div>
  <ul><li><i style="background:#2563eb"></i>Photos 19.2 GB</li><li><i style="background:#f59e0b"></i>Video 14.1 GB</li><li><i style="background:#16a34a"></i>Documents 8.7 GB</li></ul>
</div>
.stack { width: min(400px, 92vw); font: 500 14px Inter, system-ui, sans-serif; color: #344054; }
.stack p { margin: 0 0 10px; color: #101828; font-weight: 700; }
.bar { display: flex; gap: 2px; height: 12px; overflow: hidden; border-radius: 999px; background: #e4e7ec; }
.stack ul { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 12px 0 0; padding: 0; list-style: none; font-size: 13px; }
.stack li { display: flex; align-items: center; gap: 6px; }
.stack li i { width: 10px; height: 10px; border-radius: 3px; }

08. Progress Updated by JavaScript

JavaScriptLive

A bar that fills in steps while a simulated upload runs, then turns green with a done message. The progress element's value is updated from script, and a polite live region announces only the start and the finish rather than every percent.

View code
<div class="live">
  <button id="go">Start upload</button>
  <progress id="lp" max="100" value="0" aria-label="Upload progress"></progress>
  <p id="lm" aria-live="polite"></p>
</div>
.live { display: grid; gap: 12px; width: min(360px, 90vw); font: 600 14px Inter, system-ui, sans-serif; color: #c9d1e2; }
.live button { justify-self: start; padding: 9px 16px; border: 0; border-radius: 10px; background: #7c9cff; color: #0e1118; font: 700 14px Inter, sans-serif; cursor: pointer; }
.live button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.live progress { appearance: none; width: 100%; height: 10px; border: 0; border-radius: 999px; background: #1e2433; }
.live progress::-webkit-progress-bar { background: #1e2433; border-radius: 999px; }
.live progress::-webkit-progress-value { background: #7c9cff; border-radius: 999px; }
.live progress::-moz-progress-bar { background: #7c9cff; border-radius: 999px; }
.live.ok progress::-webkit-progress-value { background: #34d399; }
.live.ok progress::-moz-progress-bar { background: #34d399; }
.live p { margin: 0; min-height: 20px; }
const go = document.getElementById('go'), lp = document.getElementById('lp'), lm = document.getElementById('lm'), box = document.querySelector('.live');
go.addEventListener('click', () => {
  box.classList.remove('ok'); lp.value = 0; lm.textContent = 'Upload started'; go.disabled = true;
  const t = setInterval(() => {
    lp.value = Math.min(100, lp.value + 7);
    if (lp.value >= 100) { clearInterval(t); box.classList.add('ok'); lm.textContent = 'Upload complete'; go.disabled = false; }
  }, 150);
});

About these CSS Progress Bars

Use the progress element

<progress max="100" value="64"> is announced by screen readers with its percentage and needs no ARIA. Without a value it becomes an indeterminate busy indicator. Style it with ::-webkit-progress-bar, ::-webkit-progress-value and ::-moz-progress-bar.

When to use role=progressbar

Custom shapes such as rings or striped bars cannot be a progress element, so give the wrapper role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow and a label, and keep those values in sync with what is drawn.

Show the number in text

A bar alone is hard to read precisely. Print the value next to it, such as 64% or Step 3 of 5, and for stacked bars add a legend with each value so the meaning does not depend on telling colours apart.

Announce sparingly

When progress updates live, announce only the start and the finish in a polite live region. Reading every percentage aloud would drown out everything else for screen reader users.

Frequently asked questions

Which demos need JavaScript?

Only the live upload demo. The others are HTML and CSS.

How do I draw a circular progress ring?

Use two SVG circles and set stroke-dasharray and stroke-dashoffset from the value, as in the ring demo.

Do animated stripes affect performance?

Animating background-position is cheap, and the animation stops when reduced motion is requested.