Skip to content
WANCRAFT

G.150Data & Feedback

Activity Rings

Concentric goal rings that spring to their values, wrap past 100% with a shadowed overlap lap, and report as meters.

NewReactMotionSVGTailwindintermediate

Overview

Activity Rings draws one concentric ring per goal and springs each to its progress. Past 100% a ring keeps going as a second, darker lap whose rounded end cap casts a soft shadow onto the ring beneath, so an exceeded goal reads as layered rather than simply full. The centre shows average progress (tweened) and how many rings are closed; the legend lists each goal's value, target, unit and percentage. For assistive tech every goal is a real role="meter" with its goal as the maximum and a value text such as "620 of 500 kcal, 124%, goal reached" — the drawing itself is decorative. Colours, sizes, thickness, gap and the spring are props; with reduced motion the rings jump straight to their values.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/activity-rings.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/activity-rings.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/activity-rings.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { ActivityRings, type ActivityRing } from "@/components/ui/activity-rings";

export function ActivityRingsExample() {
  const goals: ActivityRing[] = [
    { id: "move", label: "Move", value: 420, goal: 500, unit: "kcal" },
    { id: "train", label: "Train", value: 34, goal: 30, unit: "min" },
    { id: "steps", label: "Steps", value: 6800, goal: 10000 },
  ];

  return (
    <ActivityRings rings={goals} label="Today's goals" />
  );
}

03Props

PropTypeDefaultDescription
rings{ id; label; value; goal; unit?; color? }[]—One ring per goal, outermost first (required).
labelstring"Goals"Accessible name of the meters list.
sizenumber220Maximum diameter in px; shrinks with the container.
thicknessnumber20Ring thickness in px at that size.
gapnumber4Space between rings in px.
trackOpacitynumber0.16Opacity of the unfilled track.
spring{ stiffness, damping, mass }{ stiffness: 120, damping: 18, mass: 1 }Spring each ring and the centre figure animate on.
showLegendbooleantrueVisible legend; when off the meters stay available to screen readers.
legendPosition"right" | "bottom""right"Legend beside (wide containers) or below the rings.
showCenterbooleantrueAverage progress and rings closed in the centre.
accentstring"#FF7448"Colour of the first ring without its own colour.
format(value: number) => string—Value formatter for the legend and meters.

04Features

  • 01Rings fill on a spring the first time they scroll into view; later changes spring from where each ring is
  • 02Over 100%: a darker overlap lap with a shadowed end cap (up to two laps drawn)
  • 03Centre summary: tweened average progress and rings closed
  • 04Legend with value, goal, unit and percentage
  • 05Per-ring colours with an accent-first palette; size, thickness and gap props
motion · springinput · viewport

05Accessibility

  • Chosen pattern: a labelled list of role="meter" elements, one per goal (min 0, max = goal, current value clamped to the goal).
  • Each meter's aria-valuetext gives the real value, goal, unit and percentage, including "goal reached" past 100%.
  • When the legend is hidden the meters stay in the DOM, visually hidden.
  • The SVG rings and centre figure are aria-hidden — the meters carry the same information.
  • With prefers-reduced-motion rings and the centre figure jump to their values.