Rating
The form-grade rating control. One component covers two jobs:
- Interactive —
<Rating defaultValue={0} onChange={…}>lets users pick a value. - Read-only display —
<Rating value={3.71} readOnly precision="exact">paints an average.
Both modes share the same role="slider", the same icon stack, the same label/description/helper
wiring, and the same hidden-input form integration. There's nothing to learn twice.
Overview — interactive stars and read-only average
Why this exists
Star ratings are a universal form pattern — product reviews, support surveys, NPS, content rating — but they're full of subtle mistakes when hand-rolled:
- ❌ Stars are buttons, so users hear "button, button, button, button, button" through a screen reader.
- ❌ No keyboard support, or arrow keys jump the wrong direction in RTL.
- ❌ Half-stars get geometry wrong on touch / on retina / in RTL.
- ❌ Hover preview commits on mobile because there's no real hover.
- ❌ Form libraries see no value because there's no underlying
<input>.
<Rating> fixes every one of those. The root carries role="slider" (single focus stop), each
star is aria-hidden, the W3C Slider keyboard pattern is applied with RTL-aware arrow flipping,
half-steps use clip-path on a layered fill (pixel-perfect), pointer math is gated to mouse
(touch never gets a sticky preview), and a hidden <input name=value> ships the value to every
form library.
Anatomy
┌──────────────────────────────────────────────────────┐
│ Rate your experience * ← label │
│ Tap a star to submit ← description │
│ ★ ★ ★ ★ ☆ 3 of 5 ← track + value│
│ Please rate before submitting ← error/helper │
└──────────────────────────────────────────────────────┘┌──────────────────────────────────────────────────────┐
│ Rate your experience * ← label │
│ Tap a star to submit ← description │
│ ★ ★ ★ ★ ☆ 3 of 5 ← track + value│
│ Please rate before submitting ← error/helper │
└──────────────────────────────────────────────────────┘- track — the
role="slider"row. Tab-stops here. - star (×N) —
aria-hidden, layered<empty>+<filled>SVG. The filled layer is clipped from the trailing edge so any fraction (whole / half / 0.71) renders pixel-perfect. - label / description / helper / error — wired via the shared
useFormFieldA11yhook.
Examples
Basic — uncontrolled, default scale of 5
Controlled — value lifted into parent state
Half-step — click left half of star N for N − 0.5
Read-only with exact fractional fill (averages)
Custom scales — 3, 10, anything
Custom glyph — swap any ReactNode in
Sizes — sm / md / lg
Colors — warning (default) / primary / success / danger / neutral
Show the numeric value next to the stars
allowClear — click the current value to reset
Disabled — no focus, no interaction
Read-only — static display
With label, description, and helper text
Error state — required field, aria-invalid wired in
Inside a form — hidden input ships the value
Realistic — Rating + Textarea + Submit composition
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
value | number | — | Controlled value. Pair with onChange. |
defaultValue | number | 0 | Uncontrolled initial value. |
onChange | (value, meta) => void | — | meta.source is 'click' | 'keyboard' | 'clear'. |
max | number | 5 | Number of stars. |
precision | 1 | 0.5 | 'exact' | 1 | 'exact' is read-only-only and renders arbitrary fractional fill. |
allowClear | boolean | false | Clicking the current value sets it to 0. |
icon | ReactNode | inline star | Filled glyph. |
emptyIcon | ReactNode | inline star | Empty glyph. Defaults to icon (with fill="none" outline if you don't pass one). |
readOnly | boolean | false | Static display; disables click + keyboard but stays focusable for AT. |
disabled | boolean | false | Removes focus and interaction. |
required | boolean | false | Sets aria-required + required on the hidden input. |
name | string | — | Hidden-input name for HTML form submission. Value 0 posts as empty string. |
size | 'sm' | 'md' | 'lg' | 'md' | Star size + gap density. |
color | 'warning' | 'primary' | 'success' | 'danger' | 'neutral' | 'warning' | Filled-glyph color. Empty layer always uses text-border-default. |
showValue | boolean | false | Appends "{value} of {max}" next to the stars. |
label | ReactNode | — | Visible label above the stars. |
description | ReactNode | — | Hint below the label. |
helperText | ReactNode | — | Bottom helper. Hidden when error is set. |
error | ReactNode | — | Bottom error. Sets aria-invalid="true" + data-invalid="true". |
hideLabel | boolean | false | Visually hides the label (sr-only). |
ariaLabel | string | 'Rating' | Accessible name when no visible label is provided. |
formatValueText | (value, max) => string | en string | Override the aria-valuetext announcement. Defaults to "{v} out of {max} stars". |
Keyboard
Follows the W3C Slider pattern:
| Key | Action |
|---|---|
→ / ↑ | Increase by precision (RTL: → decreases) |
← / ↓ | Decrease by precision (RTL: ← increases) |
Home | Set to 0 (when allowClear) or the smallest step |
End | Set to max |
PageUp | Increase by 1 whole star |
PageDown | Decrease by 1 whole star |
Digit 0–9 | Jump to that value (0 only when allowClear) |
A11y
- Root is
role="slider"witharia-valuemin={0},aria-valuemax={max},aria-valuenow={value},aria-valuetextpopulated viaformatValueText. Each star isaria-hidden. readOnlysetsaria-readonly="true"+tabIndex={0}(focusable for AT, not interactive).disabledsetsaria-disabled="true"+tabIndex={-1}.requiredsetsaria-required="true";errorsetsaria-invalid="true".- Label / description / helper / error are wired via
useFormFieldA11y(shared hook used by<Input>,<Textarea>,<Select>). - Error text carries
role="alert"so AT announces validation failures as they land. - Hover preview never updates
aria-valuenow— only committed values are announced. - axe-core: 0 violations across all modes (verified in
Rating.a11y.test.tsx).
RTL
- Stars visually flip — "star 1" is on the right under
dir="rtl". - Arrow keys flip —
ArrowRightdecreases in RTL, matching W3C Slider. - Pointer math mirrors — clicking the right edge in RTL still picks "star 1".
- The filled clip grows from the leading edge in both directions.
Theming
<Rating> is in the theme registry as Rating. Override slots via theme.components.Rating:
const theme = createTheme({
components: {
Rating: {
defaultProps: { color: 'primary', size: 'lg' },
styleOverrides: {
track: 'gap-2',
starFilled: 'drop-shadow-sm',
label: 'uppercase tracking-wide',
},
},
},
});const theme = createTheme({
components: {
Rating: {
defaultProps: { color: 'primary', size: 'lg' },
styleOverrides: {
track: 'gap-2',
starFilled: 'drop-shadow-sm',
label: 'uppercase tracking-wide',
},
},
},
});Slot names: wrapper, label, description, track, star, starEmpty, starFilled,
valueText, helperText.
See also
<Slider>— for arbitrary numeric ranges where stars aren't the right metaphor.<RadioGroup>— for distinct, named choices (Yes / No / Maybe) rather than ordinal scales.