Skip to content

Activity Rings

Show a few goals side by side as concentric rings, each filling toward its own target.

import { ActivityRing, ActivityRings } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use activity rings when a few goals are tracked together and each has its own target: a day's fitness goals, a team's quotas, a quarter's key results, a learning streak. Each ring is one goal, a round-capped arc over a faint track of its color, running clockwise from twelve o'clock. Two to four rings fit. To show how one whole splits into parts, use a Share Ring. For a single score on a scale, use a Gauge.

import { ActivityRings, type ActivityRing } from '@youj/react'

const rings: ActivityRing[] = [
  { label: 'Move', value: 420, max: 500, unit: 'calories', tone: 'danger' },
  { label: 'Exercise', value: 26, max: 30, unit: 'minutes', tone: 'success' },
  { label: 'Stand', value: 9, max: 12, unit: 'hours', tone: 'info' },
]

<ActivityRings rings={rings} legend="side" />

A ring is value of max, with a label, an optional unit after its goal, a tone (accent, a chart color or a status; the chart colors in turn by default) and an icon drawn at its start. Rings are listed outermost first. Past its goal a ring keeps going for a second lap (it draws up to two), and the legend and the spoken value give the full share, such as 138%.

The bowl shows the first ring's share in big figures with its name, or the share of the ring being pointed at. Pass children to put your own content there, such as a streak or a total. At size="sm" (96px) the bowl is left empty. The other sizes are default, lg and xl.

legend="side" puts the legend beside the rings when there's room and under them when there isn't. legend="below" always puts it under them. Each row gives the ring's value, goal and share. Pointing at a row, or tabbing to it, singles out its ring, and pointing at a ring singles out its row. format formats the values (whole numbers by default, { notation: 'compact' } for large figures).

The rings fill as they come into view. inView={false} fills them on mount, and { once, amount, margin } sets what counts as in view. A ring that closes plays the success cue when the provider has sound on, and sound={false} turns that off.

Reference

Propsrings, size, legend… (9)

Props

ActivityRings props
PropType
ringsRequiredreadonly ActivityRing[]

Two to four goals, outermost first.

–
sizeExtract<Size, 'sm' | 'default' | 'lg' | 'xl'>

The rings' size: sm, default, lg or xl.

default 'default'

'default'
legend'side' | 'below' | false

Where the legend sits: side puts it beside the rings when there's room and under them when not, below always under.

default false

false
formatIntl.NumberFormatOptions

Intl.NumberFormat options for the values. Default: whole numbers.

–
inViewInView

When the rings fill: false on mount; true (the default) or { once, amount, margin } as they come into view.

–
soundboolean

Play the success cue when a ring closes.

default true

true
labelstring

Names the rings for assistive tech.

default "Activity rings"

"Activity rings"
localestring

Default: the YoujProvider locale.

–
childrenReactNode

Content for the bowl instead of the figure.

–
Other props pass through: extends ComponentProps<'div'>.
StylingParts and data attributes

Styling

Parts: activity-rings (with data-size and data-yj-legend), activity-rings-dial, activity-rings-ring (with data-index, data-tone, data-closed once it reaches its goal and data-yj-over past it), activity-rings-track, activity-rings-arc, activity-rings-overlap (the second lap), activity-rings-head, activity-rings-glow, activity-rings-icon, activity-rings-center, activity-rings-value, activity-rings-caption, activity-rings-legend, activity-rings-item (with data-highlighted and data-closed), activity-rings-swatch, activity-rings-label, activity-rings-amount and activity-rings-share. In Bezel the labels are mono capital eyebrows.

MotionFirst view, new values, closing a ring, pointing · reduced motion

Motion

Motion tokens
MomentTokens
First vieweach ring fills from zero to its value
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
New valueseach ring retargets from where it is, keeping its speed
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Closing a ringa soft glow swells off the ring and fades, once
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
Pointing at a ringthe other rings and rows dim
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

The rings are drawn at their values and change in place, with no glow.

Reduced motion

The rings are drawn at their values from the first frame, new values jump, and closing a ring doesn't glow or play a cue.

AccessibilityLabels and assistive technology

Accessibility

  • The rings are a group, named by label or "Activity rings", and each ring is a meter named by its label, with its value text spoken in full: "Move: 420 of 500 calories, 84%". Past its goal, aria-valuenow holds at the goal and the text gives the real value and share.
  • The legend is a list. Each row takes focus, so the keyboard can single out a ring the same way the pointer does.
  • Its words come from the labels (activityRings, activityRingsValue), translated by the language packs.
  • A dial turns clockwise in every script, so the rings keep their direction under RTL. The bowl and the legend follow the page.