All Taste components
Animated screens
Fitness rings
A day of activity where three goal rings fill in turn and the numbers count up beside them.
rings fill in sequence · count-up numbers · flickering streak flame
Friday, 16 Oct
Today
Move420 / 600 kcal
Exercise24 / 30 min
Stand9 / 12 hrs
12-day streak
Best so far: 21 days
MTWTFSS
Steps
7,842
78% of 10,000
Distance
5.6km
↑ 0.8 km vs avg
Evening run
Today, 6:10 pm · 5.2 km · 31 min
/* globals.css
@keyframes ring-fill { from { stroke-dashoffset: var(--v); } }
@keyframes flicker {
0% { transform: scale(1) rotate(0); }
30% { transform: scale(1.04, .97) rotate(-2deg); }
60% { transform: scale(.98, 1.04) rotate(1.5deg); }
100% { transform: scale(1.03, .99) rotate(-1deg); }
}
@keyframes ember { from { opacity: .2; } to { opacity: .5; } }
*/
"use client";
import { useEffect, useState } from "react";
import { ChartColumn, ChevronRight, Dumbbell, Flame, Footprints, MapPin, User } from "lucide-react";
import { cn } from "@/lib/utils";
const rings = [
{ label: "Move", value: 420, goal: 600, unit: "kcal", r: 54, color: "text-primary", delay: 300 },
{ label: "Exercise", value: 24, goal: 30, unit: "min", r: 39, color: "text-success", delay: 850 },
{ label: "Stand", value: 9, goal: 12, unit: "hrs", r: 24, color: "text-info", delay: 1400 },
];
const week = ["M", "T", "W", "T", "F", "S", "S"];
const tabs = [
{ label: "Today", icon: Flame, current: true },
{ label: "Workouts", icon: Dumbbell },
{ label: "Progress", icon: ChartColumn },
{ label: "Profile", icon: User },
];
/** Counts from 0 to `to` once, easing out; jumps straight there with reduced motion. */
function useCountUp(to: number, delay = 0, ms = 1300) {
const [n, setN] = useState(0);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setN(to); return; }
let raf = 0;
const start = performance.now() + delay;
const tick = (t: number) => {
const k = Math.min(1, Math.max(0, (t - start) / ms));
setN(Math.round(to * (1 - Math.pow(1 - k, 3))));
if (k < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [to, delay, ms]);
return n;
}
function Ring({ r, value, goal, color, delay }: (typeof rings)[number]) {
const pct = Math.min(100, (value / goal) * 100);
return (
<g className={color} transform="rotate(-90 62 62)">
<circle cx="62" cy="62" r={r} fill="none" stroke="currentColor" strokeWidth="12" opacity="0.16" />
<circle cx="62" cy="62" r={r} fill="none" stroke="currentColor" strokeWidth="12" strokeLinecap="round" pathLength={100}
strokeDasharray={pct + " 100"} className="animate-[ring-fill_1.3s_cubic-bezier(.2,.7,.1,1)_both] motion-reduce:animate-none"
style={{ ["--v" as string]: pct, animationDelay: delay + "ms" }} />
</g>
);
}
function Legend({ label, value, goal, unit, color, delay }: (typeof rings)[number]) {
const n = useCountUp(value, delay);
return (
<div>
<dt className="flex items-center gap-1.5 text-xs text-fg-muted"><i className={cn("size-2 rounded-full bg-current", color)} />{label}</dt>
<dd className="text-[15px] font-semibold tabular-nums">{n}<span className="text-xs font-normal text-fg-muted"> / {goal} {unit}</span></dd>
</div>
);
}
export default function ActivityRingsScreen() {
const steps = useCountUp(7842, 400);
return (
<div className="mx-auto flex min-h-dvh max-w-md flex-col bg-bg font-sans text-fg">
<main className="flex-1 px-4 pb-4">
<header className="flex h-[60px] items-center">
<div className="flex-1"><p className="text-xs text-fg-muted">Friday, 16 Oct</p><h1 className="text-2xl font-semibold tracking-tight">Today</h1></div>
<span className="grid size-9 place-items-center rounded-full bg-primary-soft text-[13px] font-medium text-primary-soft-fg">KN</span>
</header>
<section aria-label="Activity rings" className="mt-1.5 flex items-center gap-3.5 rounded-lg border border-border p-3">
<svg viewBox="0 0 124 124" className="size-32 shrink-0" role="img" aria-label="Move 70%, exercise 80%, stand 75%">
{rings.map((r) => <Ring key={r.label} {...r} />)}
</svg>
<dl className="grid flex-1 gap-2">{rings.map((r) => <Legend key={r.label} {...r} />)}</dl>
</section>
<section aria-label="Streak" className="mt-3 flex items-center gap-3 rounded-lg bg-warning-soft p-3">
<span aria-hidden className="grid size-11 shrink-0 place-items-center rounded-full bg-bg text-warning">
<Flame className="size-[26px] origin-[50%_90%] animate-[flicker_2.4s_cubic-bezier(.2,.7,.1,1)_infinite_alternate] motion-reduce:animate-none" />
</span>
<div className="flex-1">
<p className="text-sm font-semibold">12-day streak</p>
<p className="text-xs text-fg-muted">Best so far: 21 days</p>
<div className="mt-1.5 flex gap-1" aria-label="Goal met Monday to Friday">
{week.map((d, i) => (
<span key={i} className={cn("grid size-[22px] place-items-center rounded-full text-[10px] font-semibold", i < 5 ? "bg-warning text-bg" : "bg-bg text-fg-subtle")}>{d}</span>
))}
</div>
</div>
</section>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-surface px-3 py-2.5">
<p className="flex items-center gap-1.5 text-xs text-fg-muted"><Footprints className="size-3.5" aria-hidden />Steps</p>
<p className="mt-1 text-lg font-semibold tabular-nums">{steps.toLocaleString("en-IN")}</p>
<p className="text-xs text-fg-muted">78% of 10,000</p>
</div>
<div className="rounded-lg bg-surface px-3 py-2.5">
<p className="flex items-center gap-1.5 text-xs text-fg-muted"><MapPin className="size-3.5" aria-hidden />Distance</p>
<p className="mt-1 text-lg font-semibold tabular-nums">5.6<span className="ml-0.5 text-xs font-medium text-fg-muted">km</span></p>
<p className="text-xs text-fg-muted">↑ 0.8 km vs avg</p>
</div>
</div>
<a href="#" className="mt-3 flex items-center gap-3 rounded-lg border border-border px-3 py-2.5">
<span className="grid size-9 place-items-center rounded-full bg-primary-soft text-primary-soft-fg"><Footprints className="size-[18px]" aria-hidden /></span>
<span className="min-w-0 flex-1"><span className="block text-sm font-semibold">Evening run</span><span className="text-xs tabular-nums text-fg-muted">Today, 6:10 pm · 5.2 km · 31 min</span></span>
<ChevronRight className="size-4 text-fg-subtle" aria-hidden />
</a>
</main>
<nav aria-label="Main" className="sticky bottom-0 grid grid-cols-4 border-t border-border bg-bg px-1.5 pb-[max(env(safe-area-inset-bottom),8px)] pt-1">
{tabs.map(({ label, icon: Icon, current }) => (
<a key={label} href="#" aria-current={current ? "page" : undefined}
className={cn("flex min-h-11 flex-col items-center justify-center gap-0.5 text-[10.5px] font-medium", current ? "text-primary" : "text-fg-subtle")}>
<Icon className="size-[22px]" aria-hidden />{label}
</a>
))}
</nav>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.