Savings goal
A savings pot with a cover photo: the goal, a ring of how much is saved, what is left and the date it is on track for, a round-ups switch, and quick amounts to add money that move the ring.
Thursday, 23 Oct
148 kcal to close your Move ring. A 25-minute walk would do it.
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<!-- Icons: Lucide (lucide.dev), ISC licence -->
<style>
.odl-activity-rings { box-sizing: border-box; inline-size: 100%; max-inline-size: 30rem; margin-inline: auto; container-type: inline-size; color: var(--ds-color-text-primary); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-activity-rings *, .odl-activity-rings *::before, .odl-activity-rings *::after { box-sizing: border-box; }
.odl-activity-rings .odl-card { --move: var(--ds-color-danger-light-9); --move-track: var(--ds-color-danger-light-4); --ex: var(--ds-color-success-light-9); --ex-track: var(--ds-color-success-light-4);
--stand: var(--ds-color-info-light-9); --stand-track: var(--ds-color-info-light-4);
display: grid; gap: var(--ds-space-stack-lg); padding: var(--ds-space-inset-xl); background: var(--ds-color-surface-raised);
border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-container); box-shadow: var(--ds-elevation-raised); }
.odl-activity-rings .odl-head { display: grid; gap: var(--ds-space-stack-xs); }
.odl-activity-rings h3 { margin: 0; font: var(--ds-text-title-lg); }
.odl-activity-rings .odl-date { margin: 0; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-activity-rings .odl-main { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--ds-space-inset-xl); }
.odl-activity-rings .odl-rings { inline-size: calc(var(--ds-size-control-lg) * 3); aspect-ratio: 1; }
.odl-activity-rings svg { display: block; inline-size: 100%; block-size: 100%; overflow: visible; }
.odl-activity-rings circle { fill: none; stroke-linecap: round; }
.odl-activity-rings .odl-arc { transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dasharray var(--ds-motion-duration-long) var(--ds-motion-easing-standard); }
.odl-activity-rings .is-move { stroke: var(--move); } .odl-activity-rings .is-move.odl-track { stroke: var(--move-track); }
.odl-activity-rings .is-ex { stroke: var(--ex); } .odl-activity-rings .is-ex.odl-track { stroke: var(--ex-track); }
.odl-activity-rings .is-stand { stroke: var(--stand); } .odl-activity-rings .is-stand.odl-track { stroke: var(--stand-track); }
.odl-activity-rings dl { display: grid; gap: var(--ds-space-stack-md); margin: 0; }
.odl-activity-rings dl > div { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--ds-space-inline-md); row-gap: var(--ds-space-stack-xs); align-items: center; }
.odl-activity-rings dl > div > svg { grid-row: 1 / 3; inline-size: var(--ds-size-icon-md); block-size: var(--ds-size-icon-md); stroke-width: var(--ds-icon-stroke-lg); color: var(--t); }
.odl-activity-rings dt { color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-activity-rings dd { margin: 0; font: var(--ds-text-title-md); font-variant-numeric: tabular-nums; }
.odl-activity-rings dd small { color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-activity-rings .odl-m { --t: var(--ds-color-danger-light-11); } .odl-activity-rings .odl-e { --t: var(--ds-color-success-light-11); } .odl-activity-rings .odl-s { --t: var(--ds-color-info-light-11); }
.odl-activity-rings dd .odl-v { color: var(--t); }
.odl-activity-rings .odl-note { display: flex; align-items: flex-start; gap: var(--ds-space-inline-md); margin: 0; padding: var(--ds-space-inset-md); border-radius: var(--ds-radius-nested); background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-primary); font: var(--ds-text-body-md); }
.odl-activity-rings .odl-note svg { flex: none; inline-size: var(--ds-size-icon-md); block-size: var(--ds-size-icon-md); stroke-width: var(--ds-icon-stroke-md); color: var(--ds-color-icon-subtle); }
.odl-activity-rings .odl-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--ds-space-inline-xs); margin: 0; padding: var(--ds-space-stack-lg) 0 0; list-style: none;
border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-separator); }
.odl-activity-rings .odl-week button { display: grid; justify-items: center; gap: var(--ds-space-stack-xs); inline-size: 100%; min-block-size: var(--ds-size-target-touch); padding: var(--ds-space-inset-xs) 0;
border: 0; border-radius: var(--ds-radius-nested); background: transparent; color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); cursor: pointer;
transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-activity-rings .odl-week button:hover { background: var(--ds-color-bg-neutral-subtle); }
.odl-activity-rings .odl-week button[aria-pressed="true"] { background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-primary); }
.odl-activity-rings .odl-week svg { inline-size: min(100%, calc(var(--ds-size-icon-lg) * 1.5)); block-size: auto; aspect-ratio: 1; }
/* The week shows the Move ring only: three rings at this size read as a blob. */
.odl-activity-rings .odl-week .is-ex, .odl-activity-rings .odl-week .is-stand { display: none; }
.odl-activity-rings .odl-week .odl-arc { transition: none; }
.odl-activity-rings button:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-activity-rings .odl-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@container (max-width: 26rem) {
.odl-activity-rings .odl-card { padding: var(--ds-space-inset-lg); }
.odl-activity-rings .odl-main { gap: var(--ds-space-inset-lg); }
.odl-activity-rings .odl-rings { inline-size: calc(var(--ds-size-control-lg) * 2.4); }
.odl-activity-rings .odl-week { gap: 0; }
}
@container (min-width: 28rem) { .odl-activity-rings .odl-rings { inline-size: calc(var(--ds-size-control-lg) * 3.8); } }
@media (prefers-reduced-motion: reduce) { .odl-activity-rings, .odl-activity-rings *, .odl-activity-rings *::before { transition: none !important; animation: none !important; } }
</style>
<section class="odl-activity-rings" aria-labelledby="odl-ar-title">
<div class="odl-card">
<div class="odl-head"><h3 id="odl-ar-title">Activity</h3><p class="odl-date" aria-live="polite">Thursday, 23 Oct</p></div>
<div class="odl-main">
<div class="odl-rings" role="img" aria-label="Move 412 of 560 calories, exercise 24 of 30 minutes, stand 9 of 12 hours">
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle class="odl-track is-move" cx="60" cy="60" r="54" stroke-width="11"/>
<circle class="odl-arc is-move" cx="60" cy="60" r="54" stroke-width="11" pathLength="100" stroke-dasharray="73.6 100"/>
<circle class="odl-track is-ex" cx="60" cy="60" r="40" stroke-width="11"/>
<circle class="odl-arc is-ex" cx="60" cy="60" r="40" stroke-width="11" pathLength="100" stroke-dasharray="80 100"/>
<circle class="odl-track is-stand" cx="60" cy="60" r="26" stroke-width="11"/>
<circle class="odl-arc is-stand" cx="60" cy="60" r="26" stroke-width="11" pathLength="100" stroke-dasharray="75 100"/>
</svg>
</div>
<dl>
<div class="odl-m"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg><dt>Move</dt><dd><span class="odl-v">412</span><small> / <span class="odl-g">560</span> kcal</small></dd></div>
<div class="odl-e"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 17 5-5-5-5"/><path d="m13 17 5-5-5-5"/></svg><dt>Exercise</dt><dd><span class="odl-v">24</span><small> / <span class="odl-g">30</span> min</small></dd></div>
<div class="odl-s"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg><dt>Stand</dt><dd><span class="odl-v">9</span><small> / <span class="odl-g">12</span> h</small></dd></div>
</dl>
</div>
<p class="odl-note"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"/><path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"/><path d="M16 17h4"/><path d="M4 13h4"/></svg><span class="odl-nt">148 kcal to close your Move ring. A 25-minute walk would do it.</span></p>
<ul class="odl-week" aria-label="This week">
<li><button type="button" data-day="0" aria-pressed="false"><span class="odl-sr">Friday 17 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="83 100"/></svg><span aria-hidden="true">F</span></button></li>
<li><button type="button" data-day="1" aria-pressed="false"><span class="odl-sr">Saturday 18 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/></svg><span aria-hidden="true">S</span></button></li>
<li><button type="button" data-day="2" aria-pressed="false"><span class="odl-sr">Sunday 19 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="46 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="20 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="58 100"/></svg><span aria-hidden="true">S</span></button></li>
<li><button type="button" data-day="3" aria-pressed="false"><span class="odl-sr">Monday 20 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="88 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="92 100"/></svg><span aria-hidden="true">M</span></button></li>
<li><button type="button" data-day="4" aria-pressed="false"><span class="odl-sr">Tuesday 21 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="100 100"/></svg><span aria-hidden="true">T</span></button></li>
<li><button type="button" data-day="5" aria-pressed="false"><span class="odl-sr">Wednesday 22 Oct</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="67 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="50 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="83 100"/></svg><span aria-hidden="true">W</span></button></li>
<li><button type="button" data-day="6" aria-pressed="true"><span class="odl-sr">Today</span><svg viewBox="0 0 120 120" aria-hidden="true"><circle class="odl-track is-move" cx="60" cy="60" r="50" stroke-width="15"/><circle class="odl-arc is-move" cx="60" cy="60" r="50" stroke-width="15" pathLength="100" stroke-dasharray="73.6 100"/><circle class="odl-track is-ex" cx="60" cy="60" r="32" stroke-width="15"/><circle class="odl-arc is-ex" cx="60" cy="60" r="32" stroke-width="15" pathLength="100" stroke-dasharray="80 100"/><circle class="odl-track is-stand" cx="60" cy="60" r="14" stroke-width="15"/><circle class="odl-arc is-stand" cx="60" cy="60" r="14" stroke-width="15" pathLength="100" stroke-dasharray="75 100"/></svg><span aria-hidden="true">T</span></button></li>
</ul>
</div>
</section>
<script>
document.querySelectorAll(".odl-activity-rings").forEach((root) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
// [move, exercise, stand] for Friday to today; goals are 560 kcal, 30 min and 12 h
const days = [[580, 34, 10, "Friday, 17 Oct"], [602, 41, 12, "Saturday, 18 Oct"], [258, 6, 7, "Sunday, 19 Oct"], [493, 30, 11, "Monday, 20 Oct"],
[611, 45, 12, "Tuesday, 21 Oct"], [375, 15, 10, "Wednesday, 22 Oct"], [412, 24, 9, "Thursday, 23 Oct"]];
const goals = [560, 30, 12], units = ["calories", "minutes", "hours"], names = ["Move", "Exercise", "Stand"];
const arcs = root.querySelectorAll(".odl-rings .odl-arc"), values = root.querySelectorAll("dd .odl-v");
const art = root.querySelector(".odl-rings"), date = root.querySelector(".odl-date"), note = root.querySelector(".odl-nt");
const show = (i) => {
const d = days[i];
d.slice(0, 3).forEach((v, k) => { arcs[k].setAttribute("stroke-dasharray", `${Math.min(100, (v / goals[k]) * 100).toFixed(1)} 100`); values[k].textContent = String(v); });
date.textContent = d[3];
art.setAttribute("aria-label", d.slice(0, 3).map((v, k) => `${names[k]} ${v} of ${goals[k]} ${units[k]}`).join(", "));
const left = goals[0] - d[0];
note.textContent = left > 0 ? (i === 6 ? `${left} kcal to close your Move ring. A 25-minute walk would do it.` : `Move ring ${left} kcal short.`) : "All of your Move goal, and then some.";
root.querySelectorAll(".odl-week button").forEach((b) => b.setAttribute("aria-pressed", String(Number(b.dataset.day) === i)));
};
root.querySelectorAll(".odl-week button").forEach((b) => b.addEventListener("click", () => show(Number(b.dataset.day))));
});
</script>
A savings pot with a cover photo: the goal, a ring of how much is saved, what is left and the date it is on track for, a round-ups switch, and quick amounts to add money that move the ring.
Budget, seats and credit as three meters, with the forecast hatched past what is used and alert thresholds marked on the bar.
Six months of spend as bars, with the current month's forecast hatched on top, a dashed budget line and a tooltip on the bar you point at.