22 CSS Progress Bars01 / 22

Pure CSSMIT licensed

Native HTML Progress Element with ARIA Fallback

The decision primer: a real <progress value="68" max="100"> on the left, a custom role="progressbar" div on the right, both at the same value so you can compare what each one buys you. Includes the complete ::-webkit-progress-bar / ::-webkit-progress-value / ::-moz-progress-bar styling block that makes the native element look designed in every engine.

Published

Live Demo
Try it

The code

<div class="pb-01">
  <input class="pb-01__sr" type="checkbox" id="pb-01-light">
  <label class="pb-01__theme" for="pb-01-light" title="Switch light or dark theme">
    <svg class="pb-01__sun" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4.4" stroke="currentColor" stroke-width="1.7"/><path d="M12 2.6v2.3M12 19.1v2.3M2.6 12h2.3M19.1 12h2.3M5.3 5.3l1.7 1.7M17 17l1.7 1.7M18.7 5.3L17 7M7 17l-1.7 1.7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
    <svg class="pb-01__moon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M20.5 14.4A8.4 8.4 0 0 1 9.6 3.5 8.6 8.6 0 1 0 20.5 14.4Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
    <span class="pb-01__vh">Switch light or dark theme</span>
  </label>

  <div class="pb-01__stage">
    <div class="pb-01__card">
      <header class="pb-01__head">
        <p class="pb-01__eyebrow"><span class="pb-01__badge">Primer</span><span class="pb-01__mono">value = 68 / max = 100</span></p>
        <h2 class="pb-01__title">Native <code>&lt;progress&gt;</code> or a custom bar?</h2>
        <p class="pb-01__lead">Same value, same visual weight, two entirely different accessibility contracts. Pick the left one unless the design makes it impossible.</p>
      </header>

      <div class="pb-01__grid">
        <section class="pb-01__pane pb-01__pane--native" aria-labelledby="pb-01-h-native">
          <h3 class="pb-01__paneTitle" id="pb-01-h-native"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M9 12.8l2.2 2.2L15.6 9.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/></svg>Native element</h3>
          <label class="pb-01__label" for="pb-01-native">Deploying <span class="pb-01__mono">astro@5.2.1</span></label>
          <progress class="pb-01__native" id="pb-01-native" value="68" max="100">68%</progress>
          <p class="pb-01__row"><span class="pb-01__mono pb-01__num">68%</span><span class="pb-01__hint">announced by AT automatically</span></p>
          <ul class="pb-01__notes">
            <li><strong>Role</strong><span>implicit <code>progressbar</code></span></li>
            <li><strong>Value</strong><span>read from <code>value</code>/<code>max</code></span></li>
            <li><strong>JS needed</strong><span>none</span></li>
          </ul>
        </section>

        <section class="pb-01__pane" aria-labelledby="pb-01-h-aria">
          <h3 class="pb-01__paneTitle"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M4 7.5h16M4 12h16M4 16.5h9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span id="pb-01-h-aria">ARIA fallback</span></h3>
          <p class="pb-01__label" id="pb-01-lbl">Deploying <span class="pb-01__mono">astro@5.2.1</span></p>
          <div class="pb-01__bar" role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-labelledby="pb-01-lbl" style="--value:68">
            <span class="pb-01__fill"><span class="pb-01__sheen"></span></span>
          </div>
          <p class="pb-01__row"><span class="pb-01__mono pb-01__num">68%</span><span class="pb-01__hint">you own every announcement</span></p>
          <ul class="pb-01__notes">
            <li><strong>Role</strong><span>explicit <code>role="progressbar"</code></span></li>
            <li><strong>Value</strong><span><code>aria-valuenow</code>, kept in sync by you</span></li>
            <li><strong>JS needed</strong><span>on every value change</span></li>
          </ul>
        </section>
      </div>

      <div class="pb-01__code">
        <p class="pb-01__codeHead"><span class="pb-01__mono">progress.css</span><span class="pb-01__tagline">three engines, one bar</span></p>
        <pre><code>.bar{ appearance:none; block-size:.9rem; border:0;
      border-radius:99px; background:var(--track); }

.bar::-webkit-progress-bar  { background:var(--track); }
.bar::-webkit-progress-value{ background:var(--accent); border-radius:99px; }
.bar::-moz-progress-bar     { background:var(--accent); border-radius:99px; }</code></pre>
      </div>

      <p class="pb-01__foot"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M12 8.2v.2m0 3.2v4.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5"/></svg>Prefer <code>&lt;progress&gt;</code> when possible — assistive tech announces value automatically.</p>
    </div>
  </div>
</div>
.pb-01 {
  --bg: #080a0f;
  --card: #0d1017;
  --card2: #12161f;
  --ink: #eef1f7;
  --muted: rgba(238,241,247,.6);
  --line: rgba(238,241,247,.11);
  --track: rgba(238,241,247,.09);
  --accent: #a78bfa;
  --accent-2: #60a5fa;
  --ok: #34d399;
  --radius: 1.35rem;
  --sans: system-ui,-apple-system,"Segoe UI",sans-serif;
  --mono: ui-monospace,"JetBrains Mono","Geist Mono",SFMono-Regular,Menlo,monospace;
  inline-size: 100%;
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(70% .18 295)) {
  .pb-01 {
    --accent: oklch(76% .16 295);
    --accent-2: oklch(74% .14 254);
    --ok: oklch(78% .15 162);
  }
}

.pb-01 *,
.pb-01 *::before,
.pb-01 *::after {
  box-sizing: border-box;
}

.pb-01__stage {
  display: grid;
  place-items: center;
  min-block-size: 100vh;
  padding: clamp(1.1rem,4vw,3.5rem);
  background: radial-gradient(120% 80% at 12% -10%,color-mix(in srgb,var(--accent) 16%,transparent),transparent 62%), radial-gradient(90% 70% at 100% 0%,color-mix(in srgb,var(--accent-2) 12%,transparent),transparent 60%);
}

.pb-01__sr {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.pb-01__vh {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.pb-01__theme {
  position: absolute;
  inset-block-start: clamp(.9rem,2.4vw,1.6rem);
  inset-inline-end: clamp(.9rem,2.4vw,1.6rem);
  z-index: 5;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  display: grid;
  place-items: center;
  cursor: pointer;
  border-radius: 99px;
  border: 1px solid var(--line);
  color: var(--ink);
  background: rgba(255,255,255,.06);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  transition: background .2s,transform .2s;
}

.pb-01__theme:hover {
  transform: translateY(-1px);
  background: rgba(255,255,255,.11);
}

.pb-01__sr:focus-visible + .pb-01__theme {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.pb-01__theme svg {
  inline-size: 1.15rem;
  block-size: 1.15rem;
}

.pb-01__sun {
  display: none;
}

.pb-01__moon {
  display: block;
}

.pb-01__card {
  inline-size: min(60rem,100%);
  display: grid;
  gap: clamp(1.1rem,2.6vw,1.8rem);
  padding: clamp(1.25rem,3.4vw,2.4rem);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: color-mix(in srgb,var(--card) 92%,transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset,0 30px 70px -40px rgba(0,0,0,.9);
}

.pb-01__head {
  display: grid;
  gap: .55rem;
}

.pb-01__eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin: 0;
}

.pb-01__badge {
  font-family: var(--mono);
  font-size: .66rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: .28rem .55rem;
  border-radius: .45rem;
  color: var(--accent);
  background: color-mix(in srgb,var(--accent) 15%,transparent);
  border: 1px solid color-mix(in srgb,var(--accent) 32%,transparent);
}

.pb-01__mono {
  font-family: var(--mono);
  font-size: .76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.pb-01__title {
  margin: 0;
  font-size: clamp(1.4rem,3.4vw,2.1rem);
  line-height: 1.12;
  letter-spacing: -.02em;
  font-weight: 650;
}

.pb-01__title code {
  font-family: var(--mono);
  font-size: .82em;
  color: var(--accent);
}

.pb-01__lead {
  margin: 0;
  max-inline-size: 46ch;
  font-size: clamp(.92rem,1.5vw,1.02rem);
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.pb-01__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(15.5rem,1fr));
  gap: clamp(.9rem,2vw,1.3rem);
}

.pb-01__pane {
  display: grid;
  gap: .7rem;
  align-content: start;
  padding: clamp(1rem,2.2vw,1.35rem);
  border-radius: 1.1rem;
  border: 1px solid var(--line);
  background: var(--card2);
}

.pb-01__pane--native {
  border-color: color-mix(in srgb,var(--accent) 34%,transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb,var(--accent) 9%,transparent);
}

.pb-01__paneTitle {
  margin: 0;
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: .82rem;
  letter-spacing: .02em;
  font-weight: 600;
}

.pb-01__paneTitle svg {
  inline-size: 1rem;
  block-size: 1rem;
  color: var(--accent);
}

.pb-01__pane--native .pb-01__paneTitle svg {
  color: var(--ok);
}

.pb-01__label {
  margin: 0;
  font-size: .86rem;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
}

.pb-01__native {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  inline-size: 100%;
  block-size: .9rem;
  border: 0;
  border-radius: 99px;
  overflow: hidden;
  background: var(--track);
  box-shadow: 0 1px 2px rgba(0,0,0,.35) inset;
}

.pb-01__native::-webkit-progress-bar {
  background: var(--track);
  border-radius: 99px;
}

.pb-01__native::-webkit-progress-value {
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius: 99px;
  transition: inline-size .4s cubic-bezier(.4,0,.2,1);
}

.pb-01__native::-moz-progress-bar {
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius: 99px;
}

.pb-01__bar {
  position: relative;
  inline-size: 100%;
  block-size: .9rem;
  border-radius: 99px;
  background: var(--track);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0,0,0,.35) inset;
}

.pb-01__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: calc(var(--value,0) * 1%);
  border-radius: 99px;
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  transition: inline-size .4s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
}

.pb-01__sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,transparent,rgba(255,255,255,.5),transparent);
  transform: translateX(-100%);
  animation: pb-01-sheen 2.6s cubic-bezier(.4,0,.2,1) infinite;
}

@keyframes pb-01-sheen {
  0% {
    transform: translateX(-100%);
  }

  60%,
    100% {
    transform: translateX(220%);
  }
}

.pb-01__row {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: .6rem;
  flex-wrap: wrap;
}

.pb-01__num {
  font-size: 1.05rem;
  color: var(--ink);
  font-weight: 600;
}

.pb-01__hint {
  font-size: .76rem;
  color: var(--muted);
}

.pb-01__notes {
  list-style: none;
  margin: .15rem 0 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.pb-01__notes li {
  display: grid;
  grid-template-columns: 5.2rem 1fr;
  gap: .7rem;
  align-items: baseline;
  padding-block: .5rem;
  border-block-start: 1px solid var(--line);
}

.pb-01__notes strong {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
}

.pb-01__notes span {
  font-size: .83rem;
  color: var(--ink);
}

.pb-01__notes code {
  font-family: var(--mono);
  font-size: .78em;
  color: var(--accent);
}

.pb-01__code {
  border-radius: 1.1rem;
  border: 1px solid var(--line);
  background: #05070b;
  overflow: hidden;
}

.pb-01__codeHead {
  margin: 0;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .65rem .95rem;
  border-block-end: 1px solid var(--line);
  background: color-mix(in srgb,var(--ink) 4%,transparent);
}

.pb-01__tagline {
  font-size: .72rem;
  color: var(--muted);
}

.pb-01__code pre {
  margin: 0;
  padding: 1rem .95rem;
  overflow-x: auto;
}

.pb-01__code code {
  font-family: var(--mono);
  font-size: clamp(.7rem,1.05vw,.81rem);
  line-height: 1.75;
  color: rgba(238,241,247,.78);
  white-space: pre;
}

.pb-01__foot {
  margin: 0;
  display: flex;
  align-items: center;
  gap: .55rem;
  font-size: .85rem;
  color: var(--muted);
}

.pb-01__foot svg {
  inline-size: 1.05rem;
  block-size: 1.05rem;
  flex: none;
  color: var(--accent);
}

.pb-01__foot code {
  font-family: var(--mono);
  font-size: .82em;
  color: var(--ink);
}

.pb-01[data-theme="light"],
.pb-01:has(#pb-01-light:checked) {
  --bg: #f6f7fb;
  --card: #ffffff;
  --card2: #f9fafc;
  --ink: #0b1020;
  --muted: rgba(11,16,32,.6);
  --line: rgba(11,16,32,.11);
  --track: rgba(11,16,32,.08);
  --accent: #7c3aed;
  --accent-2: #2563eb;
  --ok: #059669;
}

.pb-01:has(#pb-01-light:checked) .pb-01__theme {
  background: rgba(11,16,32,.05);
}

.pb-01:has(#pb-01-light:checked) .pb-01__code {
  background: #0b1020;
}

.pb-01:has(#pb-01-light:checked) .pb-01__code code {
  color: rgba(238,241,247,.82);
}

.pb-01:has(#pb-01-light:checked) .pb-01__sun {
  display: block;
}

.pb-01:has(#pb-01-light:checked) .pb-01__moon {
  display: none;
}

@media (prefers-color-scheme: light) {
  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) {
    --bg: #f6f7fb;
    --card: #ffffff;
    --card2: #f9fafc;
    --ink: #0b1020;
    --muted: rgba(11,16,32,.6);
    --line: rgba(11,16,32,.11);
    --track: rgba(11,16,32,.08);
    --accent: #7c3aed;
    --accent-2: #2563eb;
    --ok: #059669;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__code {
    background: #0b1020;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__theme {
    background: rgba(11,16,32,.05);
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__sun {
    display: block;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__moon {
    display: none;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) {
    --bg: #080a0f;
    --card: #0d1017;
    --card2: #12161f;
    --ink: #eef1f7;
    --muted: rgba(238,241,247,.6);
    --line: rgba(238,241,247,.11);
    --track: rgba(238,241,247,.09);
    --accent: #a78bfa;
    --accent-2: #60a5fa;
    --ok: #34d399;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__theme {
    background: rgba(255,255,255,.06);
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__code {
    background: #05070b;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__code code {
    color: rgba(238,241,247,.78);
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__sun {
    display: none;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__moon {
    display: block;
  }
}

@media (max-width: 52rem) {
  .pb-01__notes li {
    grid-template-columns: 1fr;
    gap: .1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pb-01 *,
    .pb-01 *::before,
    .pb-01 *::after {
    animation: none !important;
    transition: none !important;
  }

  .pb-01__sheen {
    display: none;
  }
}

@media (forced-colors: active) {
  .pb-01__card,
    .pb-01__pane,
    .pb-01__code,
    .pb-01__theme,
    .pb-01__bar {
    border-color: CanvasText;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .pb-01__fill {
    background: Highlight;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Progress Bar from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Native HTML Progress Element with ARIA Fallback
Source: https://codefronts.com/components/css-progress-bars/native-html-progress-element-with-aria-fallback/

The decision primer: a real <progress value="68" max="100"> on the left, a custom role="progressbar" div on the right, both at the same value so you can compare what each one buys you. Includes the complete ::-webkit-progress-bar / ::-webkit-progress-value / ::-moz-progress-bar styling block that makes the native element look designed in every engine.
## HTML
```html
<div class="pb-01">
  <input class="pb-01__sr" type="checkbox" id="pb-01-light">
  <label class="pb-01__theme" for="pb-01-light" title="Switch light or dark theme">
    <svg class="pb-01__sun" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4.4" stroke="currentColor" stroke-width="1.7"/><path d="M12 2.6v2.3M12 19.1v2.3M2.6 12h2.3M19.1 12h2.3M5.3 5.3l1.7 1.7M17 17l1.7 1.7M18.7 5.3L17 7M7 17l-1.7 1.7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
    <svg class="pb-01__moon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M20.5 14.4A8.4 8.4 0 0 1 9.6 3.5 8.6 8.6 0 1 0 20.5 14.4Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
    <span class="pb-01__vh">Switch light or dark theme</span>
  </label>

  <div class="pb-01__stage">
    <div class="pb-01__card">
      <header class="pb-01__head">
        <p class="pb-01__eyebrow"><span class="pb-01__badge">Primer</span><span class="pb-01__mono">value = 68 / max = 100</span></p>
        <h2 class="pb-01__title">Native <code>&lt;progress&gt;</code> or a custom bar?</h2>
        <p class="pb-01__lead">Same value, same visual weight, two entirely different accessibility contracts. Pick the left one unless the design makes it impossible.</p>
      </header>

      <div class="pb-01__grid">
        <section class="pb-01__pane pb-01__pane--native" aria-labelledby="pb-01-h-native">
          <h3 class="pb-01__paneTitle" id="pb-01-h-native"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M9 12.8l2.2 2.2L15.6 9.4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/></svg>Native element</h3>
          <label class="pb-01__label" for="pb-01-native">Deploying <span class="pb-01__mono">astro@5.2.1</span></label>
          <progress class="pb-01__native" id="pb-01-native" value="68" max="100">68%</progress>
          <p class="pb-01__row"><span class="pb-01__mono pb-01__num">68%</span><span class="pb-01__hint">announced by AT automatically</span></p>
          <ul class="pb-01__notes">
            <li><strong>Role</strong><span>implicit <code>progressbar</code></span></li>
            <li><strong>Value</strong><span>read from <code>value</code>/<code>max</code></span></li>
            <li><strong>JS needed</strong><span>none</span></li>
          </ul>
        </section>

        <section class="pb-01__pane" aria-labelledby="pb-01-h-aria">
          <h3 class="pb-01__paneTitle"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M4 7.5h16M4 12h16M4 16.5h9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg><span id="pb-01-h-aria">ARIA fallback</span></h3>
          <p class="pb-01__label" id="pb-01-lbl">Deploying <span class="pb-01__mono">astro@5.2.1</span></p>
          <div class="pb-01__bar" role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-labelledby="pb-01-lbl" style="--value:68">
            <span class="pb-01__fill"><span class="pb-01__sheen"></span></span>
          </div>
          <p class="pb-01__row"><span class="pb-01__mono pb-01__num">68%</span><span class="pb-01__hint">you own every announcement</span></p>
          <ul class="pb-01__notes">
            <li><strong>Role</strong><span>explicit <code>role="progressbar"</code></span></li>
            <li><strong>Value</strong><span><code>aria-valuenow</code>, kept in sync by you</span></li>
            <li><strong>JS needed</strong><span>on every value change</span></li>
          </ul>
        </section>
      </div>

      <div class="pb-01__code">
        <p class="pb-01__codeHead"><span class="pb-01__mono">progress.css</span><span class="pb-01__tagline">three engines, one bar</span></p>
        <pre><code>.bar{ appearance:none; block-size:.9rem; border:0;
      border-radius:99px; background:var(--track); }

.bar::-webkit-progress-bar  { background:var(--track); }
.bar::-webkit-progress-value{ background:var(--accent); border-radius:99px; }
.bar::-moz-progress-bar     { background:var(--accent); border-radius:99px; }</code></pre>
      </div>

      <p class="pb-01__foot"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M12 8.2v.2m0 3.2v4.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5"/></svg>Prefer <code>&lt;progress&gt;</code> when possible — assistive tech announces value automatically.</p>
    </div>
  </div>
</div>
```
## CSS
```css
.pb-01 {
  --bg: #080a0f;
  --card: #0d1017;
  --card2: #12161f;
  --ink: #eef1f7;
  --muted: rgba(238,241,247,.6);
  --line: rgba(238,241,247,.11);
  --track: rgba(238,241,247,.09);
  --accent: #a78bfa;
  --accent-2: #60a5fa;
  --ok: #34d399;
  --radius: 1.35rem;
  --sans: system-ui,-apple-system,"Segoe UI",sans-serif;
  --mono: ui-monospace,"JetBrains Mono","Geist Mono",SFMono-Regular,Menlo,monospace;
  inline-size: 100%;
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(70% .18 295)) {
  .pb-01 {
    --accent: oklch(76% .16 295);
    --accent-2: oklch(74% .14 254);
    --ok: oklch(78% .15 162);
  }
}

.pb-01 *,
.pb-01 *::before,
.pb-01 *::after {
  box-sizing: border-box;
}

.pb-01__stage {
  display: grid;
  place-items: center;
  min-block-size: 100vh;
  padding: clamp(1.1rem,4vw,3.5rem);
  background: radial-gradient(120% 80% at 12% -10%,color-mix(in srgb,var(--accent) 16%,transparent),transparent 62%), radial-gradient(90% 70% at 100% 0%,color-mix(in srgb,var(--accent-2) 12%,transparent),transparent 60%);
}

.pb-01__sr {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.pb-01__vh {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.pb-01__theme {
  position: absolute;
  inset-block-start: clamp(.9rem,2.4vw,1.6rem);
  inset-inline-end: clamp(.9rem,2.4vw,1.6rem);
  z-index: 5;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  display: grid;
  place-items: center;
  cursor: pointer;
  border-radius: 99px;
  border: 1px solid var(--line);
  color: var(--ink);
  background: rgba(255,255,255,.06);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  transition: background .2s,transform .2s;
}

.pb-01__theme:hover {
  transform: translateY(-1px);
  background: rgba(255,255,255,.11);
}

.pb-01__sr:focus-visible + .pb-01__theme {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.pb-01__theme svg {
  inline-size: 1.15rem;
  block-size: 1.15rem;
}

.pb-01__sun {
  display: none;
}

.pb-01__moon {
  display: block;
}

.pb-01__card {
  inline-size: min(60rem,100%);
  display: grid;
  gap: clamp(1.1rem,2.6vw,1.8rem);
  padding: clamp(1.25rem,3.4vw,2.4rem);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: color-mix(in srgb,var(--card) 92%,transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset,0 30px 70px -40px rgba(0,0,0,.9);
}

.pb-01__head {
  display: grid;
  gap: .55rem;
}

.pb-01__eyebrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin: 0;
}

.pb-01__badge {
  font-family: var(--mono);
  font-size: .66rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: .28rem .55rem;
  border-radius: .45rem;
  color: var(--accent);
  background: color-mix(in srgb,var(--accent) 15%,transparent);
  border: 1px solid color-mix(in srgb,var(--accent) 32%,transparent);
}

.pb-01__mono {
  font-family: var(--mono);
  font-size: .76rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.pb-01__title {
  margin: 0;
  font-size: clamp(1.4rem,3.4vw,2.1rem);
  line-height: 1.12;
  letter-spacing: -.02em;
  font-weight: 650;
}

.pb-01__title code {
  font-family: var(--mono);
  font-size: .82em;
  color: var(--accent);
}

.pb-01__lead {
  margin: 0;
  max-inline-size: 46ch;
  font-size: clamp(.92rem,1.5vw,1.02rem);
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

.pb-01__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(15.5rem,1fr));
  gap: clamp(.9rem,2vw,1.3rem);
}

.pb-01__pane {
  display: grid;
  gap: .7rem;
  align-content: start;
  padding: clamp(1rem,2.2vw,1.35rem);
  border-radius: 1.1rem;
  border: 1px solid var(--line);
  background: var(--card2);
}

.pb-01__pane--native {
  border-color: color-mix(in srgb,var(--accent) 34%,transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb,var(--accent) 9%,transparent);
}

.pb-01__paneTitle {
  margin: 0;
  display: flex;
  align-items: center;
  gap: .45rem;
  font-size: .82rem;
  letter-spacing: .02em;
  font-weight: 600;
}

.pb-01__paneTitle svg {
  inline-size: 1rem;
  block-size: 1rem;
  color: var(--accent);
}

.pb-01__pane--native .pb-01__paneTitle svg {
  color: var(--ok);
}

.pb-01__label {
  margin: 0;
  font-size: .86rem;
  color: var(--muted);
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
}

.pb-01__native {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  inline-size: 100%;
  block-size: .9rem;
  border: 0;
  border-radius: 99px;
  overflow: hidden;
  background: var(--track);
  box-shadow: 0 1px 2px rgba(0,0,0,.35) inset;
}

.pb-01__native::-webkit-progress-bar {
  background: var(--track);
  border-radius: 99px;
}

.pb-01__native::-webkit-progress-value {
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius: 99px;
  transition: inline-size .4s cubic-bezier(.4,0,.2,1);
}

.pb-01__native::-moz-progress-bar {
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius: 99px;
}

.pb-01__bar {
  position: relative;
  inline-size: 100%;
  block-size: .9rem;
  border-radius: 99px;
  background: var(--track);
  border: 1px solid var(--line);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0,0,0,.35) inset;
}

.pb-01__fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: calc(var(--value,0) * 1%);
  border-radius: 99px;
  background: linear-gradient(90deg,var(--accent),var(--accent-2));
  transition: inline-size .4s cubic-bezier(.4,0,.2,1);
  overflow: hidden;
}

.pb-01__sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,transparent,rgba(255,255,255,.5),transparent);
  transform: translateX(-100%);
  animation: pb-01-sheen 2.6s cubic-bezier(.4,0,.2,1) infinite;
}

@keyframes pb-01-sheen {
  0% {
    transform: translateX(-100%);
  }

  60%,
    100% {
    transform: translateX(220%);
  }
}

.pb-01__row {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: .6rem;
  flex-wrap: wrap;
}

.pb-01__num {
  font-size: 1.05rem;
  color: var(--ink);
  font-weight: 600;
}

.pb-01__hint {
  font-size: .76rem;
  color: var(--muted);
}

.pb-01__notes {
  list-style: none;
  margin: .15rem 0 0;
  padding: 0;
  display: grid;
  gap: 0;
}

.pb-01__notes li {
  display: grid;
  grid-template-columns: 5.2rem 1fr;
  gap: .7rem;
  align-items: baseline;
  padding-block: .5rem;
  border-block-start: 1px solid var(--line);
}

.pb-01__notes strong {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 500;
}

.pb-01__notes span {
  font-size: .83rem;
  color: var(--ink);
}

.pb-01__notes code {
  font-family: var(--mono);
  font-size: .78em;
  color: var(--accent);
}

.pb-01__code {
  border-radius: 1.1rem;
  border: 1px solid var(--line);
  background: #05070b;
  overflow: hidden;
}

.pb-01__codeHead {
  margin: 0;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .65rem .95rem;
  border-block-end: 1px solid var(--line);
  background: color-mix(in srgb,var(--ink) 4%,transparent);
}

.pb-01__tagline {
  font-size: .72rem;
  color: var(--muted);
}

.pb-01__code pre {
  margin: 0;
  padding: 1rem .95rem;
  overflow-x: auto;
}

.pb-01__code code {
  font-family: var(--mono);
  font-size: clamp(.7rem,1.05vw,.81rem);
  line-height: 1.75;
  color: rgba(238,241,247,.78);
  white-space: pre;
}

.pb-01__foot {
  margin: 0;
  display: flex;
  align-items: center;
  gap: .55rem;
  font-size: .85rem;
  color: var(--muted);
}

.pb-01__foot svg {
  inline-size: 1.05rem;
  block-size: 1.05rem;
  flex: none;
  color: var(--accent);
}

.pb-01__foot code {
  font-family: var(--mono);
  font-size: .82em;
  color: var(--ink);
}

.pb-01[data-theme="light"],
.pb-01:has(#pb-01-light:checked) {
  --bg: #f6f7fb;
  --card: #ffffff;
  --card2: #f9fafc;
  --ink: #0b1020;
  --muted: rgba(11,16,32,.6);
  --line: rgba(11,16,32,.11);
  --track: rgba(11,16,32,.08);
  --accent: #7c3aed;
  --accent-2: #2563eb;
  --ok: #059669;
}

.pb-01:has(#pb-01-light:checked) .pb-01__theme {
  background: rgba(11,16,32,.05);
}

.pb-01:has(#pb-01-light:checked) .pb-01__code {
  background: #0b1020;
}

.pb-01:has(#pb-01-light:checked) .pb-01__code code {
  color: rgba(238,241,247,.82);
}

.pb-01:has(#pb-01-light:checked) .pb-01__sun {
  display: block;
}

.pb-01:has(#pb-01-light:checked) .pb-01__moon {
  display: none;
}

@media (prefers-color-scheme: light) {
  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) {
    --bg: #f6f7fb;
    --card: #ffffff;
    --card2: #f9fafc;
    --ink: #0b1020;
    --muted: rgba(11,16,32,.6);
    --line: rgba(11,16,32,.11);
    --track: rgba(11,16,32,.08);
    --accent: #7c3aed;
    --accent-2: #2563eb;
    --ok: #059669;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__code {
    background: #0b1020;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__theme {
    background: rgba(11,16,32,.05);
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__sun {
    display: block;
  }

  .pb-01:not([data-theme="dark"]):not(:has(#pb-01-light:checked)) .pb-01__moon {
    display: none;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) {
    --bg: #080a0f;
    --card: #0d1017;
    --card2: #12161f;
    --ink: #eef1f7;
    --muted: rgba(238,241,247,.6);
    --line: rgba(238,241,247,.11);
    --track: rgba(238,241,247,.09);
    --accent: #a78bfa;
    --accent-2: #60a5fa;
    --ok: #34d399;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__theme {
    background: rgba(255,255,255,.06);
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__code {
    background: #05070b;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__code code {
    color: rgba(238,241,247,.78);
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__sun {
    display: none;
  }

  .pb-01:not([data-theme="light"]):has(#pb-01-light:checked) .pb-01__moon {
    display: block;
  }
}

@media (max-width: 52rem) {
  .pb-01__notes li {
    grid-template-columns: 1fr;
    gap: .1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pb-01 *,
    .pb-01 *::before,
    .pb-01 *::after {
    animation: none !important;
    transition: none !important;
  }

  .pb-01__sheen {
    display: none;
  }
}

@media (forced-colors: active) {
  .pb-01__card,
    .pb-01__pane,
    .pb-01__code,
    .pb-01__theme,
    .pb-01__bar {
    border-color: CanvasText;
    box-shadow: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  .pb-01__fill {
    background: Highlight;
  }
}
```

How this works

Start with the native element. <progress> is a real form-associated control: assistive tech announces its value and its changes without a single ARIA attribute, and it needs no JavaScript to stay in sync with itself.

<progress id="upload" value="68" max="100">68%</progress>
<!-- the text child is legacy fallback content, not a label -->

Styling it takes three engines' worth of pseudo-elements. Reset the widget with appearance:none, then paint the track and the value separately. Firefox has no track pseudo-element, so the track colour goes on the element itself:

.pb-01__native{
  appearance:none; -webkit-appearance:none;
  inline-size:100%; block-size:.9rem; border:0;
  border-radius:99px; overflow:hidden;
  background:var(--track);           /* Firefox track */
}
.pb-01__native::-webkit-progress-bar{ background:var(--track); }
.pb-01__native::-webkit-progress-value{
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius:99px;
}
.pb-01__native::-moz-progress-bar{
  background:linear-gradient(90deg,var(--accent),var(--accent-2));
  border-radius:99px;
}

When the native element is not enough — a segmented fill, a buffered second layer, a gradient sheen crossing the whole track, a value label inside the bar — rebuild it as a div and re-declare the semantics yourself. That is the right-hand card:

<div class="pb-01__bar" role="progressbar"
     aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"
     aria-label="Asset upload progress" style="--value:68">
  <span class="pb-01__fill"></span>
</div>

Never write role="progressbar" on a <progress> — the role is implicit, and duplicating it confuses older screen readers. And never put width:68% inline: the value lives in --value, the CSS owns the geometry, and one property update animates the fill.

Make it yours

  • Change the bar height in one place: block-size on .pb-01__native and .pb-01__bar. Both tracks share the same radius token, so a squarer look is one --radius edit.
  • Swap the gradient for a flat fill by replacing the linear-gradient in all three pseudo-element rules with var(--accent) — keep them in sync or engines will disagree.
  • Point max at real units instead of percentages — value="2411" max="3548" for kilobytes — and the browser does the ratio maths for you.
  • Add an indeterminate state by removing the value attribute entirely; see demo 02 for the animation that pairs with it.
  • Retune the whole palette from the six tokens at the top of the sheet; the oklch() block only lifts chroma on capable engines.
  • Drop the annotation column on narrow screens by editing the single grid-template-columns in the @media (max-width:52rem) block.

Gotchas — read before shipping

  • role="progressbar" on a <progress> element is redundant — the role is implicit and the duplicate can suppress value announcements in older AT.
  • The text inside <progress>68%</progress> is fallback content for browsers from 2011, not an accessible name. Label the element with <label for> or aria-label.
  • Firefox exposes no ::-moz-progress-bar track counterpart — set the track background on the element itself or Firefox renders a grey system track.
  • Forgetting appearance:none means Safari keeps the UA widget and ignores every colour you set.
  • A custom role="progressbar" div with no aria-valuenow is announced as an indeterminate busy region — fine for a spinner, wrong for a 68% upload.
  • aria-valuemin/aria-valuemax default to 0 and 100, but declare them anyway: a bar measuring 0-3548 KB is announced as a wildly wrong percentage otherwise.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 6+.

<progress> and role=progressbar are universally supported. The pseudo-element names differ by engine, which is why all three blocks ship together. oklch() (Chrome 111, Safari 15.4, Firefox 113) and :has() (Chrome 105, Safari 15.4, Firefox 121) are progressive enhancements behind @supports.

Techniques used in this demo

Search CodeFronts

Loading…