More Dashboards Snippets
Activity Rings — Concentric Progress Rings HTML CSS
Activity Rings · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Activity Rings — Apple-Style Concentric Goal Rings in SVG

Apple's activity rings — three nested, brightly colored arcs that fill as you close each daily goal — are one of the most recognizable data visualizations ever shipped, because they pack three progress metrics into one glanceable, motivating graphic. This snippet recreates that concentric-rings pattern in plain HTML, CSS, and SVG: any number of rings, each animating from empty to its goal percentage, with a matching value/goal legend.
Nested rings from one loop
Each ring is a pair of SVG circles — a faint full-circle track and a colored progress arc — sharing a center but with a decreasing radius so they nest inside one another. The radius for ring i steps inward by the stroke width plus a gap (CENTER − STROKE/2 − 4 − i × (STROKE + GAP)), so the rings sit concentrically with even spacing regardless of how many you add. Thick stroke-linecap: round strokes give the signature chunky, rounded-end look. It's all generated in a single loop over the ring data, so three rings or five is the same code.
The fill is stroke-dashoffset, capped at 100%
Each progress arc uses the stroke-dasharray / stroke-dashoffset technique: the dasharray is set to the circle's circumference, and the dashoffset starts at the full circumference (empty) and animates to circumference × (1 − percent) so the arc fills to exactly the goal fraction. The percent is clamped to 1, so an over-achieved goal (like 38 of 30 exercise minutes in the demo) fills the ring completely rather than overflowing — and the legend still shows the true 127%, the way Apple's rings do. The arcs are rotated −90° so they start filling from the top (12 o'clock) rather than 3 o'clock.
Animation that draws the rings on
On load, each arc starts empty and animates to its filled offset via a CSS transition with a smooth ease, using the double-requestAnimationFrame pattern to ensure the browser registers the starting state before the transition runs (otherwise it would jump straight to filled with no animation). The rings sweep into place over a second, which is both satisfying and draws the eye to each metric's progress — the motion is the moment that makes the rings feel alive rather than static.
A legend that names the numbers
Rings alone are evocative but ambiguous, so a legend lists each ring's name, current value over goal with units ("520/600 cal"), and percentage, color-matched to its arc, with a ✓ when a goal is met. This pairs the at-a-glance ring graphic with the precise numbers, covering both the "how am I doing overall" and "exactly what are my numbers" questions. Color is the link between a ring and its legend row.
Fully data-driven and re-themeable
Everything comes from the RINGS array — name, value, goal, unit, color — so adapting this to any three (or more) goals is a data edit. Apple uses Move/Exercise/Stand, but the same component works for any nested-goal display: daily tasks done, calories/protein/water, or three KPIs against targets. Swap the colors and labels and it fits any brand or metric set.
Step by step
How to Use
- 1Paste HTML, CSS, and JSAn activity-rings card renders on a dark background; the three rings animate from empty to their goal progress.
- 2Read the ringsEach concentric ring fills toward its goal — Move, Exercise, Stand — in its own color, starting from the top.
- 3Check the legendThe side legend shows each ring's value/goal with units and percentage, color-matched, with a ✓ when met.
- 4Note over-achievementA goal exceeded (38/30 min) fills its ring completely while the legend shows the true 127%.
- 5Edit the goalsChange the RINGS array (name, value, goal, unit, color) — radii, animation, and legend all recompute.
- 6Connect real dataPopulate RINGS from your fitness, habit, or goal data and re-run build() to draw the day's progress.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Edit the RINGS array — each entry has a name, value, goal, unit, and color. The build loop computes each ring's radius from its index, so adding a fourth ring nests it inside automatically; just give it a distinct color. The legend regenerates from the same array, so values and percentages update with no other changes.
Each progress arc's stroke-dasharray is set to the circle's circumference, turning the stroke into one dash as long as the ring. The stroke-dashoffset starts at the full circumference (fully hidden) and animates to circumference × (1 − percent), revealing exactly the goal fraction. The percent is clamped to 1 so an exceeded goal fills the ring completely rather than wrapping past the start.
CSS transitions only animate when a property changes after the element is rendered with its initial value. Setting dashoffset to empty and immediately to filled in the same frame would skip the animation. Two nested requestAnimationFrame calls let the browser paint the empty state first, so the subsequent change to the filled offset transitions smoothly.
Clamp only the visual fill (Math.min(1, value/goal) for the dashoffset) but compute the legend's percentage from the raw value/goal — so the ring caps at full while the number reads 127%. This mirrors Apple's behavior where the ring completes but the count keeps going, signaling over-achievement honestly.
In React, map the RINGS array to <circle> elements in JSX, computing each radius and dashoffset with useMemo, and trigger the animation with a mounted state or key; in Vue, use v-for with computed geometry; in Angular, use *ngFor. The radius/circumference/dashoffset math is plain SVG that ports unchanged — only the mount-time animation trigger uses each framework's lifecycle.
Activity Rings — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Activity Rings</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.arg-card{background:#111827;border:1px solid #1f2937;border-radius:20px;padding:24px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.arg-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
.arg-head h3{font-size:16px;font-weight:800;color:#f8fafc}
.arg-date{font-size:12px;color:#6b7280;font-weight:600}
.arg-main{display:flex;align-items:center;gap:22px;margin-top:14px}
.arg-rings{width:160px;height:160px;flex-shrink:0}
.arg-track{fill:none;stroke-linecap:round}
.arg-prog{fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)}
.arg-legend{display:flex;flex-direction:column;gap:13px}
.arg-row{display:flex;flex-direction:column;gap:2px}
.arg-row-top{display:flex;align-items:center;gap:7px}
.arg-dot{width:9px;height:9px;border-radius:50%}
.arg-name{font-size:12px;font-weight:700;color:#e5e7eb}
.arg-vals{font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px}
.arg-vals small{font-size:11px;font-weight:600;color:#6b7280}
.arg-pct{font-size:10.5px;font-weight:700;padding-left:16px}
</style>
</head>
<body>
<div class="arg-card">
<div class="arg-head">
<h3>Today's activity</h3>
<span class="arg-date">Mon, Jun 22</span>
</div>
<div class="arg-main">
<svg class="arg-rings" viewBox="0 0 200 200" id="argRings"></svg>
<div class="arg-legend" id="argLegend"></div>
</div>
</div>
<script>
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
var svg = document.getElementById('argRings');
var NS = 'http://www.w3.org/2000/svg';
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
build();
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Activity Rings</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.arg-head h3 {
font-size:16px;font-weight:800;color:#f8fafc
}
.arg-track {
fill:none;stroke-linecap:round
}
.arg-prog {
fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)
}
.arg-row {
display:flex;flex-direction:column;gap:2px
}
.arg-row-top {
display:flex;align-items:center;gap:7px
}
.arg-dot {
width:9px;height:9px;border-radius:50%
}
.arg-name {
font-size:12px;font-weight:700;color:#e5e7eb
}
.arg-vals {
font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px
}
.arg-vals small {
font-size:11px;font-weight:600;color:#6b7280
}
.arg-pct {
font-size:10.5px;font-weight:700;padding-left:16px
}
</style>
</head>
<body>
<div class="bg-[#111827] border border-[#1f2937] rounded-[20px] p-6 w-full max-w-[380px] shadow-[0_18px_44px_rgba(0,0,0,.45)]">
<div class="arg-head flex items-baseline justify-between mb-1.5">
<h3>Today's activity</h3>
<span class="text-xs text-[#6b7280] font-semibold">Mon, Jun 22</span>
</div>
<div class="flex items-center gap-[22px] mt-3.5">
<svg class="w-40 h-40 shrink-0" viewBox="0 0 200 200" id="argRings"></svg>
<div class="flex flex-col gap-[13px]" id="argLegend"></div>
</div>
</div>
<script>
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
var svg = document.getElementById('argRings');
var NS = 'http://www.w3.org/2000/svg';
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
build();
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to ActivityRings.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.arg-card{background:#111827;border:1px solid #1f2937;border-radius:20px;padding:24px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.arg-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
.arg-head h3{font-size:16px;font-weight:800;color:#f8fafc}
.arg-date{font-size:12px;color:#6b7280;font-weight:600}
.arg-main{display:flex;align-items:center;gap:22px;margin-top:14px}
.arg-rings{width:160px;height:160px;flex-shrink:0}
.arg-track{fill:none;stroke-linecap:round}
.arg-prog{fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)}
.arg-legend{display:flex;flex-direction:column;gap:13px}
.arg-row{display:flex;flex-direction:column;gap:2px}
.arg-row-top{display:flex;align-items:center;gap:7px}
.arg-dot{width:9px;height:9px;border-radius:50%}
.arg-name{font-size:12px;font-weight:700;color:#e5e7eb}
.arg-vals{font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px}
.arg-vals small{font-size:11px;font-weight:600;color:#6b7280}
.arg-pct{font-size:10.5px;font-weight:700;padding-left:16px}
`;
export default function ActivityRings() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
var svg = document.getElementById('argRings');
var NS = 'http://www.w3.org/2000/svg';
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
build();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="arg-card">
<div className="arg-head">
<h3>Today's activity</h3>
<span className="arg-date">Mon, Jun 22</span>
</div>
<div className="arg-main">
<svg className="arg-rings" viewBox="0 0 200 200" id="argRings"></svg>
<div className="arg-legend" id="argLegend"></div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function ActivityRings() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
var svg = document.getElementById('argRings');
var NS = 'http://www.w3.org/2000/svg';
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
build();
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.arg-head h3 {
font-size:16px;font-weight:800;color:#f8fafc
}
.arg-track {
fill:none;stroke-linecap:round
}
.arg-prog {
fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)
}
.arg-row {
display:flex;flex-direction:column;gap:2px
}
.arg-row-top {
display:flex;align-items:center;gap:7px
}
.arg-dot {
width:9px;height:9px;border-radius:50%
}
.arg-name {
font-size:12px;font-weight:700;color:#e5e7eb
}
.arg-vals {
font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px
}
.arg-vals small {
font-size:11px;font-weight:600;color:#6b7280
}
.arg-pct {
font-size:10.5px;font-weight:700;padding-left:16px
}
`}</style>
<div className="bg-[#111827] border border-[#1f2937] rounded-[20px] p-6 w-full max-w-[380px] shadow-[0_18px_44px_rgba(0,0,0,.45)]">
<div className="arg-head flex items-baseline justify-between mb-1.5">
<h3>Today's activity</h3>
<span className="text-xs text-[#6b7280] font-semibold">Mon, Jun 22</span>
</div>
<div className="flex items-center gap-[22px] mt-3.5">
<svg className="w-40 h-40 shrink-0" viewBox="0 0 200 200" id="argRings"></svg>
<div className="flex flex-col gap-[13px]" id="argLegend"></div>
</div>
</div>
</>
);
}<template>
<div class="arg-card">
<div class="arg-head">
<h3>Today's activity</h3>
<span class="arg-date">Mon, Jun 22</span>
</div>
<div class="arg-main">
<svg class="arg-rings" viewBox="0 0 200 200" id="argRings"></svg>
<div class="arg-legend" id="argLegend"></div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
let svg;
let NS;
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
onMounted(() => {
svg = document.getElementById('argRings');
NS = 'http://www.w3.org/2000/svg';
build();
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.arg-card{background:#111827;border:1px solid #1f2937;border-radius:20px;padding:24px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.arg-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
.arg-head h3{font-size:16px;font-weight:800;color:#f8fafc}
.arg-date{font-size:12px;color:#6b7280;font-weight:600}
.arg-main{display:flex;align-items:center;gap:22px;margin-top:14px}
.arg-rings{width:160px;height:160px;flex-shrink:0}
.arg-track{fill:none;stroke-linecap:round}
.arg-prog{fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)}
.arg-legend{display:flex;flex-direction:column;gap:13px}
.arg-row{display:flex;flex-direction:column;gap:2px}
.arg-row-top{display:flex;align-items:center;gap:7px}
.arg-dot{width:9px;height:9px;border-radius:50%}
.arg-name{font-size:12px;font-weight:700;color:#e5e7eb}
.arg-vals{font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px}
.arg-vals small{font-size:11px;font-weight:600;color:#6b7280}
.arg-pct{font-size:10.5px;font-weight:700;padding-left:16px}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-activity-rings',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="arg-card">
<div class="arg-head">
<h3>Today's activity</h3>
<span class="arg-date">Mon, Jun 22</span>
</div>
<div class="arg-main">
<svg class="arg-rings" viewBox="0 0 200 200" id="argRings"></svg>
<div class="arg-legend" id="argLegend"></div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.arg-card{background:#111827;border:1px solid #1f2937;border-radius:20px;padding:24px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(0,0,0,.45)}
.arg-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:6px}
.arg-head h3{font-size:16px;font-weight:800;color:#f8fafc}
.arg-date{font-size:12px;color:#6b7280;font-weight:600}
.arg-main{display:flex;align-items:center;gap:22px;margin-top:14px}
.arg-rings{width:160px;height:160px;flex-shrink:0}
.arg-track{fill:none;stroke-linecap:round}
.arg-prog{fill:none;stroke-linecap:round;transition:stroke-dashoffset 1s cubic-bezier(.22,1,.36,1)}
.arg-legend{display:flex;flex-direction:column;gap:13px}
.arg-row{display:flex;flex-direction:column;gap:2px}
.arg-row-top{display:flex;align-items:center;gap:7px}
.arg-dot{width:9px;height:9px;border-radius:50%}
.arg-name{font-size:12px;font-weight:700;color:#e5e7eb}
.arg-vals{font-size:16px;font-weight:800;color:#f8fafc;font-variant-numeric:tabular-nums;padding-left:16px}
.arg-vals small{font-size:11px;font-weight:600;color:#6b7280}
.arg-pct{font-size:10.5px;font-weight:700;padding-left:16px}
`]
})
export class ActivityRingsComponent implements AfterViewInit {
ngAfterViewInit(): void {
var RINGS = [
{ name: 'Move', value: 520, goal: 600, unit: 'cal', color: '#fb2c5a' },
{ name: 'Exercise',value: 38, goal: 30, unit: 'min', color: '#a3f307' },
{ name: 'Stand', value: 9, goal: 12, unit: 'hr', color: '#22d3ee' },
];
var SIZE = 200, CENTER = 100, GAP = 8, STROKE = 17;
var svg = document.getElementById('argRings');
var NS = 'http://www.w3.org/2000/svg';
function build() {
RINGS.forEach(function (ring, i) {
var r = CENTER - STROKE / 2 - 4 - i * (STROKE + GAP);
var circ = 2 * Math.PI * r;
var pct = Math.min(1, ring.value / ring.goal);
var track = document.createElementNS(NS, 'circle');
track.setAttribute('class', 'arg-track');
track.setAttribute('cx', CENTER); track.setAttribute('cy', CENTER); track.setAttribute('r', r);
track.setAttribute('stroke', ring.color); track.setAttribute('stroke-width', STROKE);
track.setAttribute('opacity', '0.18');
svg.appendChild(track);
var prog = document.createElementNS(NS, 'circle');
prog.setAttribute('class', 'arg-prog');
prog.setAttribute('cx', CENTER); prog.setAttribute('cy', CENTER); prog.setAttribute('r', r);
prog.setAttribute('stroke', ring.color); prog.setAttribute('stroke-width', STROKE);
prog.setAttribute('transform', 'rotate(-90 ' + CENTER + ' ' + CENTER + ')');
prog.setAttribute('stroke-dasharray', circ);
prog.setAttribute('stroke-dashoffset', circ); // start empty, animate to filled
svg.appendChild(prog);
// Animate after a frame so the transition runs.
requestAnimationFrame(function () {
requestAnimationFrame(function () { prog.setAttribute('stroke-dashoffset', circ * (1 - pct)); });
});
});
document.getElementById('argLegend').innerHTML = RINGS.map(function (ring) {
var pct = Math.round((ring.value / ring.goal) * 100);
return '<div class="arg-row">' +
'<div class="arg-row-top"><span class="arg-dot" style="background:' + ring.color + '"></span><span class="arg-name">' + ring.name + '</span></div>' +
'<span class="arg-vals">' + ring.value + '<small>/' + ring.goal + ' ' + ring.unit + '</small></span>' +
'<span class="arg-pct" style="color:' + ring.color + '">' + pct + '%' + (pct >= 100 ? ' ✓' : '') + '</span>' +
'</div>';
}).join('');
}
build();
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { build });
}
}