opsinjs
ComponentsHealth data display

GoalRing

A ring that shows how far a reading has come towards a goal the product set, paired with a text twin that states every figure, and rewarding no streak.

Preview

Theme
Density
Text
Status
goal-ring · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/goal-ring

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { GoalRing } from "@/components/ui/goal-ring"
<GoalRing
  label="Steps today"
  value={6200}
  goal={8000}
  unit="steps"
  category="activity"
/>

When to use it

Use it when

  • A single reading against a goal the product set, where the goal is the product's own rather than a clinical target.
  • A daily count that resets, such as steps, active minutes or water, where the words carry the exact figures.

Do not use it when

  • A composite number the product calculated, with named bands. That is a score against a scale, not progress towards a goal.Use score-dial instead.
  • One reading on its own, with its unit and when it was taken, and no goal to measure it against.Use metric-tile instead.
  • A reading against a reference range, where the meaning is which band it falls in rather than how far along a target it is.Use range-bar instead.

Clinical meaning

Asserts. One thing: this value is this far towards a goal the product set. The fraction is arithmetic over the two values, and the readout prints both in the reader's locale, unit after. The fill is drawn where the reading sits, and nothing sweeps on first paint.

Never read as. A diagnosis, a verdict on the reader, or a target opsinjs chose. A full ring means the product's goal was met, never that the reader is well, and an open ring never means something is wrong. It rewards no streak, and "normal" and "abnormal" appear in no state it draws.

Colour axis. Category only. The fill takes the identity tint of what the ring counts, the track stays neutral, and no element carries data-status.

Thresholds. None. The product owns the goal, the label and the unit; GoalRing ships no default goal and no clinical vocabulary.

Anatomy

  1. GoalRingThe root, a stacked column holding the ring above its words. No colour, no axis attribute, no fixed height.Controlled by className
  2. GoalRing.RingThe SVG, role="img", with an accessible name repeating the label, readout and progress sentence. Never focusable.Controlled by value
  3. GoalRing.TrackThe whole ring, always fully drawn in the neutral border role. Never a category or a status colour.
  4. GoalRing.FillThe arc from the top, its length the fraction reached. Carries data-category, never data-status. Absent at zero progress.Controlled by category
  5. GoalRing.ValueThe readout: the real value against the goal in the reader's locale, unit after. A value past the goal prints truthfully.Controlled by value
  6. GoalRing.DerivationThe progress sentence, derived from value and goal alone. States a met goal as a fact and never celebrates.

Examples

Progress towards a goal

A synthetic step count part-way towards a goal the product set. category="activity" tints the fill and the sentence beneath names the progress without praise.

Theme
Density
Text
Status
goal-ring-progress-to-a-goal · base base · style base-lyraOpen under the product theme

No reading yet

value={null} draws an empty ring and words that say nothing has been measured, never a zero.

Theme
Density
Text
Status
goal-ring-no-reading-yet · base base · style base-lyraOpen under the product theme

Content guidelines

Write the label as the reader's own name for what the ring counts: "Steps today", not a metric code. Keep the unit in their words too. Never phrase the copy as a challenge, and never put a streak counter or celebration beside it. When the goal is met the ring says so and stops.

Do

"Steps today", "6,200 of 8,000 steps" prints the real figures against the product's goal.

Don’t

"Great job! 3 day streak, keep it up!" beside a closed ring turns a reading into a game.

Accessibility

Checked by pnpm run check:a11y on every commit.

  • No element carries both axes: the fill carries data-category alone and nothing stamps data-status.
  • No raw colour literal and no ramp step. The track is the neutral border role and the fill a category -accent role.
  • No type size in pixels: label, readout and sentence are the subheadline, title2 and footnote steps.

Audited against WCAG 2.2 AA. A source pass and a rendered pass both ran and found nothing to change in the source. This is an author audit, not an independent review, and clinical review is still pending. The graphic is role="img" with an accessible name that speaks the label, the value against the goal and the progress sentence, all repeated in the visible words beside it. Nothing takes focus or animates on first paint, so there is no keyboard contract. Three states stay three: a finite value draws a fill, null says "no reading yet", a broken value says "not available". Under forced colours the track maps to CanvasText and the fill to Highlight.

Residual. No contrast pair is measured, so the page prints the contrast pairs as unmeasured rather than as a pass. The arc and track repeat every fact in the visible text and the accessible name, so under 1.4.11 they are not graphical objects required to understand the content. A read-only display carries no live region, so a screen reader is not told when the value changes over time; the product owns whether to wrap the ring in a live region.

KeyActionNotes
TabSkips the whole componentA labelled image, not a control.
Shift+TabSkips the whole componentNever in the focus order.
Arrow keysNothingNothing to select or change.
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every part but the svg, alwaysgoal-ring, goal-ring-track, goal-ring-fill, goal-ring-words, goal-ring-label, goal-ring-value, goal-ring-derivation
data-categoryOn the fill, while it is drawn with a known categoryThe category name, such as activity. Absent when no category is given.

API reference

Prop

Type

Generated from GoalRingProps in registry/bases/base/goal-ring.tsx.

value is number | null: null is no reading and is never drawn as zero, and a non-finite value is announced as "not available". A goal of zero or less draws an empty ring and raises a development warning. category outside the six known names is refused with a development warning and the fill falls back to neutral.

  • ScoreDial is a composite number against a scale with named bands.
  • MetricTile is one reading on its own, with no goal to measure it against.
  • RangeBar is a reading against a reference range, where the band it lands in is the meaning.

On this page