Open demo

CSS code

HTML
<div><span class="lbl">Reading progress from a scroll timeline</span><div class="card">
<div class="md">
  <article class="md-read" tabindex="0" aria-labelledby="md-article">
    <div class="md-read-bar" aria-hidden="true"></div>
    <div class="md-read-body">
      <h3 id="md-article">How we cut our CI build time in half</h3>
      <small>Engineering blog · 6 min read · Scroll this box</small>
      <p>Our main pipeline had crept up to 19 minutes. Nobody decided that; it happened one reasonable step at a time, a new test suite here, a second linter there, until waiting for CI became the longest part of shipping a one-line fix.</p>
      <p>The first win was boring: we cached the dependency install on the lockfile hash. Cold installs took four minutes and happened on every run. Now they happen about twice a week.</p>
      <p>Next we split the 2,400 end-to-end tests across six runners by their recorded duration rather than by file name. The slowest shard used to finish eleven minutes after the fastest one. Today the gap is under forty seconds.</p>
      <p>We also stopped building the documentation site on pull requests that do not touch it. A path filter took ten minutes to write and saves three minutes on most runs.</p>
      <p>The result is a nine-minute pipeline, and the habit of checking the build timeline every sprint so it does not creep back up.</p>
    </div>
  </article>
</div></div></div>

<div><span class="lbl">Counters with @property</span><div class="card">
<div class="md">
  <div class="md-count" style="--v:84">
    <div class="md-head"><b id="md-course">Accessibility for Designers</b><span class="md-num" aria-hidden="true"></span></div>
    <div class="md-bar" role="progressbar" aria-labelledby="md-course" aria-valuenow="84" aria-valuemin="0" aria-valuemax="100"><i></i></div>
    <p class="md-note">21 of 25 lessons · Certificate at 100%</p>
  </div>
  <div class="md-count md-pair md-gap" style="--v:62">
    <div class="md-ring-wrap"><div class="md-ring" role="progressbar" aria-labelledby="md-trip" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-valuetext="62%, 1,550 of 2,500 euros saved"></div><span class="md-num" aria-hidden="true"></span></div>
    <div><b id="md-trip">Savings for Lisbon trip</b><p>€1,550 of €2,500 saved · €190 a month</p></div>
  </div>
</div></div></div>

<div><span class="lbl">Native progress element</span><div class="card">
<div class="md">
  <div class="md-head"><label for="md-zip">Compressing invoices-2025.zip</label><span aria-hidden="true">58%</span></div>
  <progress id="md-zip" max="100" value="58">58%</progress>
  <div class="md-head md-gap"><label for="md-check">Checking for updates</label></div>
  <progress id="md-check" max="100">Checking for updates</progress>
  <p class="md-note">A progress element without a value is indeterminate, and CSS can target it with :indeterminate.</p>
</div></div></div>
CSS
/* Progress bars that CSS drives on its own, no script:
   1. Reading progress: a named scroll timeline on the scroller animates a sticky bar. Where animation-timeline is
      not supported (Firefox today) the bar is simply not shown. For a whole page use animation-timeline: scroll(root).
   2. Counters: a registered integer (@property --md-n) animates from 0 to the value, and the same number paints the
      fill and prints the digits through a CSS counter. Under reduced motion it shows the final value straight away.
   3. Native <progress>: determinate and indeterminate, styled for WebKit/Blink and Firefox. */
.md{--md-accent:#0e7490;--md-track:#e3e6e1;--md-ink:#1b2430;--md-muted:#565d66;color:var(--md-ink)}
.md-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:4px 12px;font-size:.92rem}
.md-head b,.md-head label{font-weight:600}
.md-head span{font-weight:700;color:var(--md-accent);font-variant-numeric:tabular-nums}
.md-note{margin-top:10px;font-size:.82rem;color:var(--md-muted)}
/* 1. reading progress */
.md-read{position:relative;max-height:260px;overflow-y:auto;border:1px solid #e7e2d8;border-radius:12px;background:#fffdf9;scroll-timeline:--md-read block}
.md-read:focus-visible{outline:2px solid var(--md-accent);outline-offset:3px}
.md-read-bar{display:none;position:sticky;top:0;z-index:1;height:4px;background:var(--md-accent);transform-origin:0 50%}
@supports (animation-timeline:scroll()){
  .md-read-bar{display:block;animation:md-grow linear both;animation-timeline:--md-read}
}
@keyframes md-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.md-read-body{padding:16px 20px 20px}
.md-read h3{font-size:1.05rem;line-height:1.3;margin-bottom:4px}
.md-read small{display:block;margin-bottom:12px;font-size:.78rem;color:var(--md-muted)}
.md-read p{font-size:.9rem;line-height:1.6;color:#343c47}
.md-read p+p{margin-top:10px}
/* 2. @property counters */
@property --md-n{syntax:"<integer>";inherits:true;initial-value:0}
.md-count{--md-n:var(--v);animation:md-count 1.4s cubic-bezier(.2,.7,.2,1) both}
@keyframes md-count{from{--md-n:0}}
.md-num::after{counter-reset:md-n var(--md-n);content:counter(md-n) "%"}
.md-bar{height:10px;margin-top:12px;border-radius:999px;background:var(--md-track);overflow:hidden}
.md-bar>i{display:block;height:100%;width:calc(var(--md-n) * 1%);border-radius:inherit;background:var(--md-accent)}
.md-pair{display:flex;flex-wrap:wrap;align-items:center;gap:18px 24px}
.md-pair>div:last-child{flex:1 1 200px}
.md-ring{flex:none;display:grid;place-items:center;width:96px;height:96px;border-radius:50%;
  background:conic-gradient(#b45309 calc(var(--md-n) * 1%),var(--md-track) 0);
  -webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8px));
  mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8px))}
.md-ring-wrap{position:relative;flex:none;display:grid;place-items:center}
.md-ring-wrap .md-num{position:absolute;font-size:1.3rem;font-weight:700;font-variant-numeric:tabular-nums}
.md-pair b{display:block;font-size:.95rem}
.md-pair p{margin-top:4px;font-size:.84rem;color:var(--md-muted)}
/* 3. native <progress> */
.md progress{-webkit-appearance:none;appearance:none;display:block;width:100%;height:10px;margin-top:12px;border:0;border-radius:999px;
  background:var(--md-track);color:var(--md-accent);overflow:hidden}
.md progress::-webkit-progress-bar{border-radius:999px;background:var(--md-track)}
.md progress::-webkit-progress-value{border-radius:999px;background:var(--md-accent)}
.md progress::-moz-progress-bar{border-radius:999px;background:var(--md-accent)}
/* indeterminate: no value attribute; a segment slides across the element's own background */
.md progress:indeterminate{background:linear-gradient(var(--md-accent) 0 0) no-repeat,var(--md-track);background-size:40% 100%;
  animation:md-slide 1.4s ease-in-out infinite}
.md progress:indeterminate::-webkit-progress-bar{background:transparent}
.md progress:indeterminate::-moz-progress-bar{background:transparent}
@keyframes md-slide{from{background-position:-70% 0}to{background-position:170% 0}}
.md-gap{margin-top:22px}
@media (prefers-reduced-motion:reduce){
  .md-count{animation:none}
  .md progress:indeterminate{animation:none;background:repeating-linear-gradient(45deg,var(--md-accent) 0 6px,#7fb3c1 6px 12px)}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Reading progress from a scroll timeline</span><div class="card demo-card"><div class="card-body p-4">
<div class="md">
  <article class="md-read position-relative overflow-y-auto border rounded-3" tabindex="0" aria-labelledby="md-article">
    <div class="md-read-bar position-sticky top-0 z-1" aria-hidden="true"></div>
    <div class="md-read-body">
      <h3 id="md-article">How we cut our CI build time in half</h3>
      <small>Engineering blog · 6 min read · Scroll this box</small>
      <p>Our main pipeline had crept up to 19 minutes. Nobody decided that; it happened one reasonable step at a time, a new test suite here, a second linter there, until waiting for CI became the longest part of shipping a one-line fix.</p>
      <p>The first win was boring: we cached the dependency install on the lockfile hash. Cold installs took four minutes and happened on every run. Now they happen about twice a week.</p>
      <p>Next we split the 2,400 end-to-end tests across six runners by their recorded duration rather than by file name. The slowest shard used to finish eleven minutes after the fastest one. Today the gap is under forty seconds.</p>
      <p>We also stopped building the documentation site on pull requests that do not touch it. A path filter took ten minutes to write and saves three minutes on most runs.</p>
      <p>The result is a nine-minute pipeline, and the habit of checking the build timeline every sprint so it does not creep back up.</p>
    </div>
  </article>
</div></div></div></div>

<div><span class="lbl">Counters with @property</span><div class="card demo-card"><div class="card-body p-4">
<div class="md">
  <div class="md-count" style="--v:84">
    <div class="md-head d-flex flex-wrap justify-content-between align-items-baseline column-gap-3 row-gap-1"><b class="fw-semibold" id="md-course">Accessibility for Designers</b><span class="md-num fw-bold md-val" aria-hidden="true"></span></div>
    <div class="progress" role="progressbar" aria-labelledby="md-course" aria-valuenow="84" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar"></div></div>
    <p class="md-note">21 of 25 lessons · Certificate at 100%</p>
  </div>
  <div class="md-count md-pair md-gap d-flex flex-wrap align-items-center row-gap-3 column-gap-4" style="--v:62">
    <div class="md-ring-wrap position-relative flex-shrink-0 d-grid" style="place-items:center"><div class="md-ring" role="progressbar" aria-labelledby="md-trip" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-valuetext="62%, 1,550 of 2,500 euros saved"></div><span class="md-num" aria-hidden="true"></span></div>
    <div><b class="d-block" id="md-trip">Savings for Lisbon trip</b><p>€1,550 of €2,500 saved · €190 a month</p></div>
  </div>
</div></div></div></div>

<div><span class="lbl">Native progress element</span><div class="card demo-card"><div class="card-body p-4">
<div class="md">
  <div class="md-head d-flex flex-wrap justify-content-between align-items-baseline column-gap-3 row-gap-1"><label class="fw-semibold" for="md-zip">Compressing invoices-2025.zip</label><span class="fw-bold md-val" aria-hidden="true">58%</span></div>
  <progress id="md-zip" max="100" value="58">58%</progress>
  <div class="md-head md-gap d-flex flex-wrap justify-content-between align-items-baseline column-gap-3 row-gap-1"><label class="fw-semibold" for="md-check">Checking for updates</label></div>
  <progress id="md-check" max="100">Checking for updates</progress>
  <p class="md-note">A progress element without a value is indeterminate, and CSS can target it with :indeterminate.</p>
</div></div></div></div>
CSS
/* Progress bars that CSS drives on its own, no script, next to Bootstrap 5 (.progress for the counter bar, utilities
   for layout). Bootstrap has no styles for the native <progress> element, so those rules are here too.
   1. Reading progress: a named scroll timeline on the scroller animates a sticky bar. Where animation-timeline is
      not supported (Firefox today) the bar is simply not shown. For a whole page use animation-timeline: scroll(root).
   2. Counters: a registered integer (@property --md-n) animates from 0 to the value, and the same number paints the
      fill and prints the digits through a CSS counter. Under reduced motion it shows the final value straight away.
   3. Native <progress>: determinate and indeterminate, styled for WebKit/Blink and Firefox. */
.md{--md-accent:#0e7490;--md-track:#e3e6e1;--md-ink:#1b2430;--md-muted:#565d66;color:var(--md-ink)}
.md-head{font-size:.92rem}
.md-head .md-val{color:var(--md-accent);font-variant-numeric:tabular-nums}
.md-note{margin:10px 0 0;font-size:.82rem;color:var(--md-muted)}
/* 1. reading progress */
.md-read{max-height:260px;--bs-border-color:#e7e2d8;background:#fffdf9;scroll-timeline:--md-read block}
.md-read:focus-visible{outline:2px solid var(--md-accent);outline-offset:3px}
.md-read-bar{display:none;height:4px;background:var(--md-accent);transform-origin:0 50%}
@supports (animation-timeline:scroll()){
  .md-read-bar{display:block;animation:md-grow linear both;animation-timeline:--md-read}
}
@keyframes md-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}
.md-read-body{padding:16px 20px 20px}
.md-read h3{font-size:1.05rem;line-height:1.3;font-weight:700;margin-bottom:4px}
.md-read small{display:block;margin-bottom:12px;font-size:.78rem;color:var(--md-muted)}
.md-read p{font-size:.9rem;line-height:1.6;color:#343c47}
.md-read p{margin:0}
.md-read p+p{margin-top:10px}
/* 2. @property counters */
@property --md-n{syntax:"<integer>";inherits:true;initial-value:0}
.md-count{--md-n:var(--v);animation:md-count 1.4s cubic-bezier(.2,.7,.2,1) both}
@keyframes md-count{from{--md-n:0}}
.md-num::after{counter-reset:md-n var(--md-n);content:counter(md-n) "%"}
.md .progress{--bs-progress-height:10px;--bs-progress-bg:var(--md-track);--bs-progress-bar-bg:var(--md-accent);--bs-progress-border-radius:999px;margin-top:12px}
.md-count .progress-bar{width:calc(var(--md-n) * 1%);border-radius:inherit;transition:none}
.md-pair>div:last-child{flex:1 1 200px}
.md-ring{flex:none;display:grid;place-items:center;width:96px;height:96px;border-radius:50%;
  background:conic-gradient(#b45309 calc(var(--md-n) * 1%),var(--md-track) 0);
  -webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8px));
  mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8px))}
.md-ring-wrap .md-num{position:absolute;font-size:1.3rem;font-weight:700;font-variant-numeric:tabular-nums}
.md-pair b{font-size:.95rem}
.md-pair p{margin:4px 0 0;font-size:.84rem;color:var(--md-muted)}
/* 3. native <progress> */
.md progress{-webkit-appearance:none;appearance:none;display:block;width:100%;height:10px;margin-top:12px;border:0;border-radius:999px;
  background:var(--md-track);color:var(--md-accent);overflow:hidden}
.md progress::-webkit-progress-bar{border-radius:999px;background:var(--md-track)}
.md progress::-webkit-progress-value{border-radius:999px;background:var(--md-accent)}
.md progress::-moz-progress-bar{border-radius:999px;background:var(--md-accent)}
/* indeterminate: no value attribute; a segment slides across the element's own background */
.md progress:indeterminate{background:linear-gradient(var(--md-accent) 0 0) no-repeat,var(--md-track);background-size:40% 100%;
  animation:md-slide 1.4s ease-in-out infinite}
.md progress:indeterminate::-webkit-progress-bar{background:transparent}
.md progress:indeterminate::-moz-progress-bar{background:transparent}
@keyframes md-slide{from{background-position:-70% 0}to{background-position:170% 0}}
.md-gap{margin-top:22px}
@media (prefers-reduced-motion:reduce){
  .md-count{animation:none}
  .md progress:indeterminate{animation:none;background:repeating-linear-gradient(45deg,var(--md-accent) 0 6px,#7fb3c1 6px 12px)}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5f6368] uppercase">Reading progress from a scroll timeline</span><div class="rounded-2xl border border-md-line bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(27,36,48,.04)]">
<div>
  <article class="relative max-h-[260px] overflow-y-auto rounded-xl border border-md-line bg-[#fffdf9] [scroll-timeline:--md-read_block] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-md-accent" tabindex="0" aria-labelledby="md-article">
    <div class="md-read-bar sticky top-0 z-[1] h-1 origin-left bg-md-accent" aria-hidden="true"></div>
    <div class="px-5 pt-4 pb-5">
      <h3 class="mb-1 text-[1.05rem] leading-[1.3] font-bold" id="md-article">How we cut our CI build time in half</h3>
      <small class="mb-3 block text-[.78rem] text-md-muted">Engineering blog · 6 min read · Scroll this box</small>
      <p class="text-[.9rem] leading-[1.6] text-[#343c47]">Our main pipeline had crept up to 19 minutes. Nobody decided that; it happened one reasonable step at a time, a new test suite here, a second linter there, until waiting for CI became the longest part of shipping a one-line fix.</p>
      <p class="mt-2.5 text-[.9rem] leading-[1.6] text-[#343c47]">The first win was boring: we cached the dependency install on the lockfile hash. Cold installs took four minutes and happened on every run. Now they happen about twice a week.</p>
      <p class="mt-2.5 text-[.9rem] leading-[1.6] text-[#343c47]">Next we split the 2,400 end-to-end tests across six runners by their recorded duration rather than by file name. The slowest shard used to finish eleven minutes after the fastest one. Today the gap is under forty seconds.</p>
      <p class="mt-2.5 text-[.9rem] leading-[1.6] text-[#343c47]">We also stopped building the documentation site on pull requests that do not touch it. A path filter took ten minutes to write and saves three minutes on most runs.</p>
      <p class="mt-2.5 text-[.9rem] leading-[1.6] text-[#343c47]">The result is a nine-minute pipeline, and the habit of checking the build timeline every sprint so it does not creep back up.</p>
    </div>
  </article>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5f6368] uppercase">Counters with @property</span><div class="rounded-2xl border border-md-line bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(27,36,48,.04)]">
<div>
  <div class="md-count [--v:84]">
    <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-[.92rem]"><b class="font-semibold" id="md-course">Accessibility for Designers</b><span class="font-bold text-md-accent tabular-nums after:[counter-reset:md-n_var(--md-n)] after:content-[counter(md-n)_'%']" aria-hidden="true"></span></div>
    <div class="mt-3 h-2.5 overflow-hidden rounded-full bg-md-track" role="progressbar" aria-labelledby="md-course" aria-valuenow="84" aria-valuemin="0" aria-valuemax="100"><div class="h-full w-[calc(var(--md-n)*1%)] rounded-full bg-md-accent"></div></div>
    <p class="mt-2.5 text-[.82rem] text-md-muted">21 of 25 lessons · Certificate at 100%</p>
  </div>
  <div class="md-count mt-[22px] flex flex-wrap items-center gap-x-6 gap-y-[18px] [--v:62]">
    <div class="relative grid flex-none place-items-center"><div class="size-24 rounded-full bg-[conic-gradient(var(--color-md-amber)_calc(var(--md-n)*1%),var(--color-md-track)_0)] [mask:radial-gradient(farthest-side,transparent_calc(100%_-_9px),#000_calc(100%_-_8px))]" role="progressbar" aria-labelledby="md-trip" aria-valuenow="62" aria-valuemin="0" aria-valuemax="100" aria-valuetext="62%, 1,550 of 2,500 euros saved"></div><span class="absolute text-[1.3rem] font-bold tabular-nums after:[counter-reset:md-n_var(--md-n)] after:content-[counter(md-n)_'%']" aria-hidden="true"></span></div>
    <div class="flex-[1_1_200px]"><b class="block text-[.95rem]" id="md-trip">Savings for Lisbon trip</b><p class="mt-1 text-[.84rem] text-md-muted">€1,550 of €2,500 saved · €190 a month</p></div>
  </div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5f6368] uppercase">Native progress element</span><div class="rounded-2xl border border-md-line bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(27,36,48,.04)]">
<div>
  <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-[.92rem]"><label class="font-semibold" for="md-zip">Compressing invoices-2025.zip</label><span class="font-bold text-md-accent tabular-nums" aria-hidden="true">58%</span></div>
  <progress class="md-progress mt-3" id="md-zip" max="100" value="58">58%</progress>
  <div class="mt-[22px] flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 text-[.92rem]"><label class="font-semibold" for="md-check">Checking for updates</label></div>
  <progress class="md-progress mt-3" id="md-check" max="100">Checking for updates</progress>
  <p class="mt-2.5 text-[.82rem] text-md-muted">A progress element without a value is indeterminate, and CSS can target it with :indeterminate.</p>
</div></div></div>
CSS
@theme {
  --color-md-accent: #0e7490;
  --color-md-amber: #b45309;
  --color-md-track: #e3e6e1;
  --color-md-ink: #1b2430;
  --color-md-muted: #565d66;
  --color-md-line: #e7e2d8;
}
@keyframes md-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes md-count { from { --md-n: 0; } }
@keyframes md-slide { from { background-position: -70% 0; } to { background-position: 170% 0; } }
/* 1. Reading progress: the shorthand must come before animation-timeline, so it lives in one utility. */
@utility md-read-bar {
  display: none;
  @supports (animation-timeline: scroll()) {
    display: block; animation: md-grow linear both; animation-timeline: --md-read;
  }
}
/* 2. A registered integer that can be animated; the fill and the digits both read it. */
@property --md-n { syntax: "<integer>"; inherits: true; initial-value: 0; }
@utility md-count {
  --md-n: var(--v);
  animation: md-count 1.4s cubic-bezier(.2,.7,.2,1) both;
  @media (prefers-reduced-motion: reduce) { animation: none; }
}
/* 3. Native <progress>: WebKit/Blink and Firefox each need their own pseudo-elements. */
@utility md-progress {
  -webkit-appearance: none; appearance: none; display: block; width: 100%; height: 10px; border: 0; border-radius: 999px;
  background: var(--color-md-track); color: var(--color-md-accent); overflow: hidden;
  &::-webkit-progress-bar { border-radius: 999px; background: var(--color-md-track); }
  &::-webkit-progress-value { border-radius: 999px; background: var(--color-md-accent); }
  &::-moz-progress-bar { border-radius: 999px; background: var(--color-md-accent); }
  &:indeterminate {
    background: linear-gradient(var(--color-md-accent) 0 0) no-repeat, var(--color-md-track); background-size: 40% 100%;
    animation: md-slide 1.4s ease-in-out infinite;
    @media (prefers-reduced-motion: reduce) {
      animation: none; background: repeating-linear-gradient(45deg, var(--color-md-accent) 0 6px, #7fb3c1 6px 12px);
    }
  }
  &:indeterminate::-webkit-progress-bar { background: transparent; }
  &:indeterminate::-moz-progress-bar { background: transparent; }
}

About this set

Three progress indicators that usually take JavaScript, done in CSS. Reading progress from a scroll timeline gives an article a bar that fills as you scroll: the scroller declares a named scroll-timeline and a sticky bar inside it runs a scaleX animation on that timeline, so there is no scroll listener. For a whole page, the same bar uses animation-timeline: scroll(root). Where animation-timeline is not supported, the bar is simply not shown and the article reads as normal. Counters with @property register –md-n as an integer and animate it from 0 to the value, so one number drives the fill width, a conic ring and the digits, which are printed through a CSS counter. Under reduced motion the animation is off and the final value shows straight away. Native progress element styles a real progress element through ::-webkit-progress-bar, ::-webkit-progress-value and ::-moz-progress-bar, and leaves the value off the second one to make it indeterminate; CSS finds that state with :indeterminate and slides a segment across the element’s own background. The native element is labelled with a plain label for.

What’s included

  • Reading progress bar driven by a CSS scroll timeline
  • Clean fallback where animation-timeline is missing
  • Numbers that count up through a registered @property integer
  • One value drives the fill, a ring and the printed digits
  • Native progress element, determinate and indeterminate, styled cross-browser
  • No JavaScript in any version

Accessibility

The reading bar is decorative (aria-hidden), and its article is keyboard-focusable with a visible outline so it can be scrolled with the arrow keys. The counter bar and ring are role="progressbar" elements with a fixed aria-valuenow, while the animated digits are aria-hidden. The native progress elements are labelled with label for, and the indeterminate one has no value, so browsers expose it as a progress bar with no current value. Under prefers-reduced-motion the count-up and the indeterminate slide stop.

Customise it

Change --md-accent and --md-track on .md, and --v on each .md-count, in the plain CSS and Bootstrap versions. In Tailwind edit the md- theme colours and the [--v:84] class, and the md-read-bar, md-count and md-progress utilities.