Skip to content

Progress Ring Activity Rings

The rings share one grid cell and nest by size alone. Each ring tints its own track by setting --sv-progress-ring-track-clr to a transparent color-mix() of its fill color, and light-dark() adapts the palette to the color scheme. Assigning value animates each fill, with --sv-progress-ring-dur and --sv-progress-ring-ease shaping the motion.

  • Move 425 / 500 kcal
  • Exercise 18 / 30 min
  • Stand 5 / 12 h

Code

The following example assumes that you have imported the component and set up the theme.

Activity Rings
<div class="activity-card">
25 collapsed lines
<div class="activity-rings">
<sv-progress-ring class="move" aria-label="Move goal" value="85"></sv-progress-ring>
<sv-progress-ring class="exercise" aria-label="Exercise goal" value="60"></sv-progress-ring>
<sv-progress-ring class="stand" aria-label="Stand goal" value="40"></sv-progress-ring>
</div>
<ul class="activity-legend">
<li class="move">
Move
<output>425</output>
/ 500 kcal
</li>
<li class="exercise">
Exercise
<output>18</output>
/ 30 min
</li>
<li class="stand">
Stand
<output>5</output>
/ 12 h
</li>
</ul>
<button type="button" class="randomize-button">Randomize</button>
</div>
<script type="application/x-typescript" module>
13 collapsed lines
const card = document.querySelector(".activity-card")!;
const goals = { move: 500, exercise: 30, stand: 12 };
card.querySelector(".randomize-button")?.addEventListener("click", () => {
for (const [name, goal] of Object.entries(goals)) {
const ring = card.querySelector<ProgressRing>(`sv-progress-ring.${name}`)!;
const output = card.querySelector<HTMLOutputElement>(`.activity-legend .${name} output`)!;
ring.value = 10 + Math.random() * 90;
output.value = String(Math.round((ring.value / 100) * goal));
}
});
</script>
<style>
102 collapsed lines
.activity-card {
--move-color: light-dark(#e11d48, #fa2e55);
--exercise-color: light-dark(#65a30d, #a3e635);
--stand-color: light-dark(#0891b2, #22d3ee);
display: grid;
gap: 1.5rem;
justify-items: center;
max-inline-size: 22rem;
padding: clamp(1rem, 4vw, 2rem);
margin: auto;
background-color: var(--sv-gray-7);
border: 1px solid var(--sv-gray-5);
border-radius: var(--sv-bdr-rad-lg);
}
.activity-rings {
display: grid;
place-items: center;
> * {
grid-area: 1 / 1;
}
sv-progress-ring {
--sv-progress-ring-track-thickness: 12px;
--sv-progress-ring-fill-thickness: 12px;
--sv-progress-ring-track-clr: color-mix(in srgb, var(--ring-color) 25%, transparent);
--sv-progress-ring-fill-clr: var(--ring-color);
--sv-progress-ring-dur: 1.2s;
--sv-progress-ring-ease: cubic-bezier(0.22, 1, 0.36, 1);
}
.move {
--ring-color: var(--move-color);
inline-size: 9rem;
}
.exercise {
--ring-color: var(--exercise-color);
inline-size: 7rem;
}
.stand {
--ring-color: var(--stand-color);
inline-size: 5rem;
}
}
.activity-legend {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
padding: 0;
margin: 0;
font-size: 0.8rem;
list-style: none;
li {
display: inline-flex;
gap: 0.4rem;
align-items: center;
}
li::before {
inline-size: 0.6rem;
block-size: 0.6rem;
content: "";
background-color: var(--ring-color);
border-radius: 50%;
}
.move {
--ring-color: var(--move-color);
}
.exercise {
--ring-color: var(--exercise-color);
}
.stand {
--ring-color: var(--stand-color);
}
}
.randomize-button {
padding: 0.5em 1.5em;
font: inherit;
color: var(--sv-accent-content);
cursor: pointer;
background-color: var(--sv-accent);
border: none;
border-radius: var(--sv-bdr-rad-md);
&:focus-visible {
outline: 2px solid var(--sv-accent);
outline-offset: 2px;
}
}
</style>