Rating

Rating

View in Storybook

A Rating is a 5-pip single-select control for rating something on a 1-5 scale — for example, how comfortable an exercise felt. It looks like a star-rating widget, but with 5 square pips in a row instead of stars, and each step has its own color from a graduated bad-to-good scale instead of a single fill color. Selection is discrete, not cumulative: picking step 3 lights up only pip 3, the rest stay neutral — it is not a magnitude fill like a star rating.


Overview


When to use

  • To collect a single subjective rating on a fixed 1-5 scale (e.g. "how did this exercise feel?").
  • To display a previously recorded rating as a static, read-only pip cluster (e.g. a history of past sessions shown as a row of compact dot clusters).
  • Anywhere a full-width, mobile-friendly segmented row is preferable to a native <select> or a row of radio buttons.

When not to use

  • To represent progress through a process — use ProgressBar instead.
  • To select one option from an open-ended or non-numeric list — use Select.
  • When the value should visually accumulate (like a star rating filling up to the chosen value) — Rating is intentionally discrete, only the selected pip is colored.

States

Unset
Selected (3)
Read-only comfort trend (5 past sessions)

Unsetvalue={0}, all 5 pips render neutral with a raised bevel.

Selected — the chosen pip renders with its step color (--color-rating-1 through --color-rating-5) and a sunken bevel, as if pressed in; the other 4 pips stay neutral and raised.

Read-only — pass readOnly; the control renders as a static, non-interactive display instead of a radiogroup, for showing a previously recorded rating (e.g. a comfort trend of past sessions).

Sizes are shown separately:

md — full touch-target row
sm — compact dot cluster

size="md" (default) renders full touch-target pips that stretch to fill their container — the shape used for active rating input. size="sm" renders a compact, fixed-size dot cluster — meant for read-only trend or summary displays, e.g. several past sessions' ratings shown side by side.


Labels

Pass labels — a tuple of 5 short strings — to render text inside each pip instead of a plain color block:

<Rating
value={rating}
onChange={setRating}
labels={['Awful', 'Bad', 'Ok', 'Good', 'Nice']}
aria-label="Level of comfort"
/>

Only rendered at size="md" — there's no room for text in the compact sm dots. Each pip's accessible name includes the label too (e.g. "Good (4 of 5)").


Accessibility

When interactive (not readOnly), Rating uses Radix UI's RadioGroup primitive under the hood:

  • Keyboard: Arrow keys move selection between pips; Tab moves focus onto and off of the group once (roving tabindex).
  • Screen readers: The group is announced using the required aria-label prop (e.g. "Rate how this exercise felt"), and each pip is announced as a radio button with its position (e.g. "3 of 5") and checked state.
  • Focus ring: Visible outline on keyboard focus via focus-visible.
<Rating value={rating} onChange={setRating} aria-label="Rate how this exercise felt" />

When readOnly, there is nothing to select, so Rating does not render a radiogroup. Instead the whole cluster gets role="img" with the given aria-label summarizing the value, and the individual pips are hidden from assistive technology:

<Rating value={4} readOnly size="sm" aria-label="Comfort rating: 4 out of 5" />

aria-label has no default and is required in both modes — always describe what is being rated.


Controls

Interactive controls are available in Storybook. Open Storybook


Props


Token mapping

PropertyToken
background (unselected pip)--color-accent-primary (matches Button/Panel — recedes into the card, contour comes from the bevel only)
label text (when labels is passed)--color-text-on-accent — reads on the neutral background and all 5 rating colors alike
background (selected, step 1 — Uncomfortable)--color-rating-1
background (selected, step 2 — Somewhat off)--color-rating-2
background (selected, step 3 — Neutral)--color-rating-3
background (selected, step 4 — Comfortable)--color-rating-4
background (selected, step 5 — Excellent)--color-rating-5
bevel, unselected (top-left outer)--sunken-outer-light
bevel, unselected (bottom-right outer)--sunken-outer-dark
bevel, unselected (top-left inner)--sunken-inner-light
bevel, unselected (bottom-right inner)--sunken-inner-dark
bevel, selected (top-left outer)--sunken-outer-dark
bevel, selected (bottom-right outer)--sunken-outer-light
bevel, selected (top-left inner)--sunken-inner-dark
bevel, selected (bottom-right inner)--sunken-inner-light
focus ring--color-accent-primary (via Tailwind's outline-primary)

Do's and don'ts

Do

  • Pass a specific, descriptive aria-label that names what's being rated (e.g. "Rate how this exercise felt"), not a generic one like "Rating".
  • Use readOnly for any display-only usage — never render an interactive radiogroup the user can't meaningfully act on.
  • Let a full-width size="md" Rating stretch to fill its container on mobile; don't wrap it in a fixed narrow box.

Don't

  • Treat the selected pip as a fill level — only one pip is ever colored, regardless of value.
  • Reuse the 5 --color-rating-* tokens for anything other than this bad-to-good comfort scale.
  • Omit onChange on an interactive instance and expect clicks to do anything — without it the control is effectively read-only.

Changelog

DateChange
2026-07-22Added optional labels prop — short text rendered inside each pip at size="md" (e.g. "Awful"/"Bad"/"Ok"/"Good"/"Nice"), so pips don't read as empty color blocks
2026-07-22Fixed readOnly at size="md" rendering as a compact fixed-width cluster instead of the same full-width grid as the interactive version — toggling readOnly (e.g. an exercise card switching to its "complete" state) visibly shrank and left-aligned the control. Read-only now mirrors the interactive layout exactly, size for size
2026-07-22Unselected pip background changed from --color-bg-input to --color-accent-primary — it was drawing more attention than the selected state; now it matches Button/Panel and recedes, relying on the bevel alone for contour
2026-07-22Fixed steps 3 and 4 mapping to near-identical colors (--color-text-secondary/--color-accent-primary) that also matched Panel's own background, making a selected pip disappear on any card. Step 3 now uses --color-bg-page, step 4 a new --primitive-accent-success-backed value
2026-07-22Initial component created — 5-pip discrete single-select built on Radix UI's RadioGroup, with a read-only display mode and sm/md sizes