24 CSS Animated Cards14 / 24

Pure CSSMIT licensed

Progress Ring Fill on Load

Circular progress done two ways, side by side: a conic-gradient ring animated with a registered @property angle (no SVG at all), and the classic SVG stroke-dasharray arc. Both fill on load with an ease that decelerates into the target, both carry role="meter" with real values, and both re-theme from a single hue token.

Published

Live Demo
Try it

The code

<section class="ac-14" aria-label="Progress ring fill on load demo">
  <div class="ac-14__stage">
    <header class="ac-14__head">
      <p class="ac-14__eyebrow">Two routes, same ring</p>
      <h2 class="ac-14__title">Circular progress that fills on load</h2>
    </header>
    <div class="ac-14__grid">
      <article class="ac-14__card">
        <div class="ac-14__wrap" role="meter" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100" aria-label="Storage used, 72 percent">
          <span class="ac-14__ring" aria-hidden="true"></span>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n" aria-hidden="true"></b><i class="ac-14__pct" aria-hidden="true">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Storage used</h3><p class="ac-14__sub">1.44 TB of 2 TB · conic-gradient + @property</p></div>
      </article>
      <article class="ac-14__card">
        <div class="ac-14__svgwrap" role="meter" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100" aria-label="Course completion, 66 percent">
          <svg viewBox="0 0 120 120" class="ac-14__svg" aria-hidden="true">
            <circle class="ac-14__track" cx="60" cy="60" r="50"/>
            <circle class="ac-14__arc" cx="60" cy="60" r="50" pathLength="100"/>
          </svg>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n2" aria-hidden="true"></b><i class="ac-14__pct">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Course completion</h3><p class="ac-14__sub">22 of 33 lessons · SVG stroke-dasharray</p></div>
      </article>
      <article class="ac-14__card">
        <div class="ac-14__wrap" role="meter" aria-valuenow="41" aria-valuemin="0" aria-valuemax="100" aria-label="Monthly goal, 41 percent">
          <span class="ac-14__ring ac-14__ring--alt" aria-hidden="true"></span>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n3" aria-hidden="true"></b><i class="ac-14__pct" aria-hidden="true">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Monthly goal</h3><p class="ac-14__sub">$8,200 of $20,000 · segmented mask</p></div>
      </article>
    </div>
    <p class="ac-14__chip" aria-hidden="true">@property &lt;angle&gt; · conic-gradient sweep · radial mask donut · pathLength circle · role=meter</p>
  </div>
</section>
@property --ac-14-ang {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --ac-14-ang3 {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --ac-14-a {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --ac-14-b {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --ac-14-c {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

.ac-14,
.ac-14 *,
.ac-14 *::before,
.ac-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ac-14 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --ac-14-bg: oklch(0.16 0.014 285);
  --ac-14-card: oklch(0.21 0.017 285);
  --ac-14-ink: oklch(0.97 0.004 280);
  --ac-14-mut: oklch(0.69 0.02 285);
  --ac-14-acc: oklch(0.75 0.17 300);
  --ac-14-track: oklch(1 0 0/.09);
  --ac-14-line: oklch(1 0 0/.11);
  background: var(--ac-14-bg);
  color: var(--ac-14-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.ac-14__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,2.6vw,28px);
  padding: clamp(20px,5vw,58px);
  background: radial-gradient(90% 60% at 50% 0%,color-mix(in oklch,var(--ac-14-acc) 12%,transparent),transparent 62%);
}

.ac-14__head {
  display: grid;
  justify-items: center;
  gap: 9px;
  text-align: center;
}

.ac-14__eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ac-14-acc);
}

.ac-14__title {
  font-size: clamp(22px,3.2vw,36px);
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.ac-14__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(210px,1fr));
  gap: 14px;
  width: min(100%,860px);
}

.ac-14__card {
  display: grid;
  justify-items: center;
  gap: 15px;
  padding: 22px 18px;
  border: 1px solid var(--ac-14-line);
  border-radius: 20px;
  background: var(--ac-14-card);
  text-align: center;
  animation: ac-14-rise .8s cubic-bezier(.16,1,.3,1) both;
  animation-delay: calc(var(--ac-14-i,0) * 110ms);
}

.ac-14__card:nth-child(2) {
  --ac-14-i: 1;
}

.ac-14__card:nth-child(3) {
  --ac-14-i: 2;
}

@keyframes ac-14-rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.ac-14__wrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
}

.ac-14__ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,var(--ac-14-acc) var(--ac-14-ang),var(--ac-14-track) 0);
  -webkit-mask: radial-gradient(farthest-side,transparent 67%,#000 68%);
  mask: radial-gradient(farthest-side,transparent 67%,#000 68%);
  animation: ac-14-fill 1.7s cubic-bezier(.16,1,.3,1) .2s both;
}

@keyframes ac-14-fill {
  from {
    --ac-14-ang: 0deg;
  }

  to {
    --ac-14-ang: 259deg;
  }
}

.ac-14__ring--alt {
  background: conic-gradient(from 0deg,oklch(0.82 0.16 88) var(--ac-14-ang3),var(--ac-14-track) 0);
  -webkit-mask: radial-gradient(farthest-side,transparent 67%,#000 68%) ,repeating-conic-gradient(#000 0 8deg,transparent 8deg 10deg);
  mask: radial-gradient(farthest-side,transparent 67%,#000 68%),repeating-conic-gradient(#000 0 8deg,transparent 8deg 10deg);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  animation: ac-14-fill3 1.7s cubic-bezier(.16,1,.3,1) .42s both;
}

@keyframes ac-14-fill3 {
  from {
    --ac-14-ang3: 0deg;
  }

  to {
    --ac-14-ang3: 148deg;
  }
}

.ac-14__hole {
  position: absolute;
  display: grid;
  grid-auto-flow: column;
  place-content: center;
  justify-items: center;
  align-items: baseline;
  gap: 1px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: -.04em;
  line-height: 1;
}

.ac-14__hole--abs {
  inset: 0;
}

.ac-14__n,
.ac-14__n2,
.ac-14__n3 {
  font-size: 32px;
}

.ac-14__pct {
  font-style: normal;
  font-size: 15px;
  color: var(--ac-14-mut);
  font-weight: 640;
}

.ac-14__n {
  counter-reset: ac-14-a var(--ac-14-a);
  animation: ac-14-ka 1.7s cubic-bezier(.16,1,.3,1) .2s both;
}

.ac-14__n::after {
  content: counter(ac-14-a);
}

.ac-14__n2 {
  counter-reset: ac-14-b var(--ac-14-b);
  animation: ac-14-kb 1.7s cubic-bezier(.16,1,.3,1) .31s both;
}

.ac-14__n2::after {
  content: counter(ac-14-b);
}

.ac-14__n3 {
  counter-reset: ac-14-c var(--ac-14-c);
  animation: ac-14-kc 1.7s cubic-bezier(.16,1,.3,1) .42s both;
}

.ac-14__n3::after {
  content: counter(ac-14-c);
}

@keyframes ac-14-ka {
  from {
    --ac-14-a: 0;
  }

  to {
    --ac-14-a: 72;
  }
}

@keyframes ac-14-kb {
  from {
    --ac-14-b: 0;
  }

  to {
    --ac-14-b: 66;
  }
}

@keyframes ac-14-kc {
  from {
    --ac-14-c: 0;
  }

  to {
    --ac-14-c: 41;
  }
}

.ac-14__svgwrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
}

.ac-14__svg {
  width: 132px;
  height: 132px;
  rotate: -90deg;
}

.ac-14__track {
  fill: none;
  stroke: var(--ac-14-track);
  stroke-width: 11;
}

.ac-14__arc {
  fill: none;
  stroke: oklch(0.80 0.15 195);
  stroke-width: 11;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: ac-14-arc 1.7s cubic-bezier(.16,1,.3,1) .31s forwards;
  filter: drop-shadow(0 0 8px color-mix(in oklch,oklch(0.80 0.15 195) 45%,transparent));
}

@keyframes ac-14-arc {
  to {
    stroke-dashoffset: 34;
  }
}

.ac-14__meta {
  display: grid;
  gap: 4px;
}

.ac-14__name {
  font-size: 15px;
  font-weight: 640;
  letter-spacing: -.02em;
}

.ac-14__sub {
  font-size: 11.5px;
  color: var(--ac-14-mut);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}

.ac-14__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ac-14-mut);
  padding: 8px 15px;
  border: 1px solid var(--ac-14-line);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,700px);
}

@media (prefers-reduced-motion: reduce) {
  .ac-14 * {
    animation-duration: 1ms !important;
  }
}
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 Animated Card 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: Progress Ring Fill on Load
Source: https://codefronts.com/components/css-animated-cards/progress-ring-fill-on-load/

Circular progress done two ways, side by side: a conic-gradient ring animated with a registered @property angle (no SVG at all), and the classic SVG stroke-dasharray arc. Both fill on load with an ease that decelerates into the target, both carry role="meter" with real values, and both re-theme from a single hue token.
## HTML
```html
<section class="ac-14" aria-label="Progress ring fill on load demo">
  <div class="ac-14__stage">
    <header class="ac-14__head">
      <p class="ac-14__eyebrow">Two routes, same ring</p>
      <h2 class="ac-14__title">Circular progress that fills on load</h2>
    </header>
    <div class="ac-14__grid">
      <article class="ac-14__card">
        <div class="ac-14__wrap" role="meter" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100" aria-label="Storage used, 72 percent">
          <span class="ac-14__ring" aria-hidden="true"></span>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n" aria-hidden="true"></b><i class="ac-14__pct" aria-hidden="true">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Storage used</h3><p class="ac-14__sub">1.44 TB of 2 TB · conic-gradient + @property</p></div>
      </article>
      <article class="ac-14__card">
        <div class="ac-14__svgwrap" role="meter" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100" aria-label="Course completion, 66 percent">
          <svg viewBox="0 0 120 120" class="ac-14__svg" aria-hidden="true">
            <circle class="ac-14__track" cx="60" cy="60" r="50"/>
            <circle class="ac-14__arc" cx="60" cy="60" r="50" pathLength="100"/>
          </svg>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n2" aria-hidden="true"></b><i class="ac-14__pct">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Course completion</h3><p class="ac-14__sub">22 of 33 lessons · SVG stroke-dasharray</p></div>
      </article>
      <article class="ac-14__card">
        <div class="ac-14__wrap" role="meter" aria-valuenow="41" aria-valuemin="0" aria-valuemax="100" aria-label="Monthly goal, 41 percent">
          <span class="ac-14__ring ac-14__ring--alt" aria-hidden="true"></span>
          <span class="ac-14__hole ac-14__hole--abs"><b class="ac-14__n3" aria-hidden="true"></b><i class="ac-14__pct" aria-hidden="true">%</i></span>
        </div>
        <div class="ac-14__meta"><h3 class="ac-14__name">Monthly goal</h3><p class="ac-14__sub">$8,200 of $20,000 · segmented mask</p></div>
      </article>
    </div>
    <p class="ac-14__chip" aria-hidden="true">@property &lt;angle&gt; · conic-gradient sweep · radial mask donut · pathLength circle · role=meter</p>
  </div>
</section>
```
## CSS
```css
@property --ac-14-ang {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --ac-14-ang3 {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --ac-14-a {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --ac-14-b {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --ac-14-c {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

.ac-14,
.ac-14 *,
.ac-14 *::before,
.ac-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ac-14 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --ac-14-bg: oklch(0.16 0.014 285);
  --ac-14-card: oklch(0.21 0.017 285);
  --ac-14-ink: oklch(0.97 0.004 280);
  --ac-14-mut: oklch(0.69 0.02 285);
  --ac-14-acc: oklch(0.75 0.17 300);
  --ac-14-track: oklch(1 0 0/.09);
  --ac-14-line: oklch(1 0 0/.11);
  background: var(--ac-14-bg);
  color: var(--ac-14-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.ac-14__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,2.6vw,28px);
  padding: clamp(20px,5vw,58px);
  background: radial-gradient(90% 60% at 50% 0%,color-mix(in oklch,var(--ac-14-acc) 12%,transparent),transparent 62%);
}

.ac-14__head {
  display: grid;
  justify-items: center;
  gap: 9px;
  text-align: center;
}

.ac-14__eyebrow {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ac-14-acc);
}

.ac-14__title {
  font-size: clamp(22px,3.2vw,36px);
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.ac-14__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(210px,1fr));
  gap: 14px;
  width: min(100%,860px);
}

.ac-14__card {
  display: grid;
  justify-items: center;
  gap: 15px;
  padding: 22px 18px;
  border: 1px solid var(--ac-14-line);
  border-radius: 20px;
  background: var(--ac-14-card);
  text-align: center;
  animation: ac-14-rise .8s cubic-bezier(.16,1,.3,1) both;
  animation-delay: calc(var(--ac-14-i,0) * 110ms);
}

.ac-14__card:nth-child(2) {
  --ac-14-i: 1;
}

.ac-14__card:nth-child(3) {
  --ac-14-i: 2;
}

@keyframes ac-14-rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.ac-14__wrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
}

.ac-14__ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,var(--ac-14-acc) var(--ac-14-ang),var(--ac-14-track) 0);
  -webkit-mask: radial-gradient(farthest-side,transparent 67%,#000 68%);
  mask: radial-gradient(farthest-side,transparent 67%,#000 68%);
  animation: ac-14-fill 1.7s cubic-bezier(.16,1,.3,1) .2s both;
}

@keyframes ac-14-fill {
  from {
    --ac-14-ang: 0deg;
  }

  to {
    --ac-14-ang: 259deg;
  }
}

.ac-14__ring--alt {
  background: conic-gradient(from 0deg,oklch(0.82 0.16 88) var(--ac-14-ang3),var(--ac-14-track) 0);
  -webkit-mask: radial-gradient(farthest-side,transparent 67%,#000 68%) ,repeating-conic-gradient(#000 0 8deg,transparent 8deg 10deg);
  mask: radial-gradient(farthest-side,transparent 67%,#000 68%),repeating-conic-gradient(#000 0 8deg,transparent 8deg 10deg);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  animation: ac-14-fill3 1.7s cubic-bezier(.16,1,.3,1) .42s both;
}

@keyframes ac-14-fill3 {
  from {
    --ac-14-ang3: 0deg;
  }

  to {
    --ac-14-ang3: 148deg;
  }
}

.ac-14__hole {
  position: absolute;
  display: grid;
  grid-auto-flow: column;
  place-content: center;
  justify-items: center;
  align-items: baseline;
  gap: 1px;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: -.04em;
  line-height: 1;
}

.ac-14__hole--abs {
  inset: 0;
}

.ac-14__n,
.ac-14__n2,
.ac-14__n3 {
  font-size: 32px;
}

.ac-14__pct {
  font-style: normal;
  font-size: 15px;
  color: var(--ac-14-mut);
  font-weight: 640;
}

.ac-14__n {
  counter-reset: ac-14-a var(--ac-14-a);
  animation: ac-14-ka 1.7s cubic-bezier(.16,1,.3,1) .2s both;
}

.ac-14__n::after {
  content: counter(ac-14-a);
}

.ac-14__n2 {
  counter-reset: ac-14-b var(--ac-14-b);
  animation: ac-14-kb 1.7s cubic-bezier(.16,1,.3,1) .31s both;
}

.ac-14__n2::after {
  content: counter(ac-14-b);
}

.ac-14__n3 {
  counter-reset: ac-14-c var(--ac-14-c);
  animation: ac-14-kc 1.7s cubic-bezier(.16,1,.3,1) .42s both;
}

.ac-14__n3::after {
  content: counter(ac-14-c);
}

@keyframes ac-14-ka {
  from {
    --ac-14-a: 0;
  }

  to {
    --ac-14-a: 72;
  }
}

@keyframes ac-14-kb {
  from {
    --ac-14-b: 0;
  }

  to {
    --ac-14-b: 66;
  }
}

@keyframes ac-14-kc {
  from {
    --ac-14-c: 0;
  }

  to {
    --ac-14-c: 41;
  }
}

.ac-14__svgwrap {
  position: relative;
  display: grid;
  place-items: center;
  width: 132px;
  height: 132px;
}

.ac-14__svg {
  width: 132px;
  height: 132px;
  rotate: -90deg;
}

.ac-14__track {
  fill: none;
  stroke: var(--ac-14-track);
  stroke-width: 11;
}

.ac-14__arc {
  fill: none;
  stroke: oklch(0.80 0.15 195);
  stroke-width: 11;
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: ac-14-arc 1.7s cubic-bezier(.16,1,.3,1) .31s forwards;
  filter: drop-shadow(0 0 8px color-mix(in oklch,oklch(0.80 0.15 195) 45%,transparent));
}

@keyframes ac-14-arc {
  to {
    stroke-dashoffset: 34;
  }
}

.ac-14__meta {
  display: grid;
  gap: 4px;
}

.ac-14__name {
  font-size: 15px;
  font-weight: 640;
  letter-spacing: -.02em;
}

.ac-14__sub {
  font-size: 11.5px;
  color: var(--ac-14-mut);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;
}

.ac-14__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ac-14-mut);
  padding: 8px 15px;
  border: 1px solid var(--ac-14-line);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,700px);
}

@media (prefers-reduced-motion: reduce) {
  .ac-14 * {
    animation-duration: 1ms !important;
  }
}
```

How this works

The modern route needs no SVG. Register an angle, paint a conic gradient with it, and animate:

@property --ac-14-ang { syntax: '<angle>'; inherits: false; initial-value: 0deg }
.ring {
  background: conic-gradient(var(--fill) var(--ac-14-ang), var(--track) 0);
  mask: radial-gradient(farthest-side, transparent 68%, black 69%);
  animation: ac-14-fill 1.6s cubic-bezier(.16,1,.3,1) both;
}
@keyframes ac-14-fill { to { --ac-14-ang: 259deg } }   /* 72% of 360 */

Without the @property registration the angle is an untyped string and the gradient snaps. The mask punches the hole — a radial gradient that is transparent to 68% and opaque after, which is a donut in one declaration and needs no second element.

The SVG route remains the most compatible, and the same pathLength trick applies to circles:

<circle pathLength="100" stroke-dasharray="100" style="--to: 34" />
@keyframes ac-14-arc { to { stroke-dashoffset: var(--to) } }  /* 66% shown */

Both rings start at 12 o'clock (rotate(-90deg) on the SVG, from 0deg on the conic) and decelerate into the final value. The numeric readout counts in step with the arc using the @property counter technique from demo 12, so number and geometry always agree.

Make it yours

  • Threshold colour: --fill: oklch(from var(--acc) l c calc(h - 40)) above 80%, or simply swap the token with a [data-level="high"] attribute for a red danger ring.
  • Segmented ring: add repeating-conic-gradient as a mask layer with mask-composite: intersect for a ticked gauge.
  • Multi-ring (activity rings): stack three masked divs at different inset sizes, each with its own angle property and a 120 ms delay.
  • Indeterminate state: rotate the whole ring with a partial conic sweep while waiting for a real value.

Gotchas — read before shipping

  • A conic gradient without a registered @property angle jumps instead of sweeping — registration is mandatory, not optional polish.
  • mask needs -webkit-mask for older Safari; both are included here. Forgetting it produces a solid disc, not a ring.
  • SVG circles start at 3 o'clock. Rotate -90deg (and set transform-box: fill-box if you rotate the element rather than the SVG).
  • place-items and place-content are not interchangeable. place-items aligns each item inside its own track; auto tracks still stretch to fill the container, so a centred label splits into halves. Use place-content: center to centre the tracks themselves — and note align-items: baseline against a full-height row anchors to the top, not the middle.
  • mask clips descendants, not just the element — a percentage label placed inside the masked ring is erased by the transparent centre. Keep the readout as a sibling in a positioned wrapper, as here.
  • A ring is a graphic: give it role="meter" with aria-valuenow/min/max and a text label, or screen-reader users get nothing at all.
  • Don't animate the ring on every re-render of a dashboard; a progress element that re-fills constantly is noise. Animate on first paint and transition thereafter.

Browser support

ChromeSafariFirefoxEdge
114+17.5+128+114+

Floor set by oklch(), color-mix(), @property, text-wrap as this demo is written. The core technique itself goes back further — Chrome 85+.

@property (needed for the conic route): Chrome 85, Safari 16.4, Firefox 128. CSS mask: Chrome 120, Safari 15.4, Firefox 53 (with -webkit- prefix earlier). The SVG route works back to Chrome 80 / Safari 14 / Firefox 75 everywhere.

Techniques used in this demo

Search CodeFronts

Loading…