Rating
Captures a score in four shapes — star scale, numeric NPS scale, emoji reaction picker and binary thumbs — as a keyboard-operable form control, or as a read-only display that paints an exact fractional average.
pnpm add @tessinaui/uiWhen to use
Rating is a form control: it holds a value, it can be submitted, and it is operable from the keyboard.
- A score the person picks and you submit. Name the group with
label, and addnameto submit the value with the form. - A quick sentiment or satisfaction answer. Pick the variant that matches
the question:
starfor a review score,numericfor NPS or CSAT,reactionfor a feeling,binaryfor "was this helpful?". - A score you are only displaying. Set
readOnly: it becomes a single labelled image rather than a set of unusable radios, and paints an exact fractional average. - A fixed, short scale. Every variant has a bounded set of steps. Keep the scale the same wherever the score appears so results compare.
- A visible name for the question. Say what is being rated, and pair the
score with a readout (
showValue) or end labels (lowLabel,highLabel) when the scale needs anchoring.
When not to use
- For thumbs in a chat or AI message toolbar. Those act immediately, hold no value, and belong in the message's action row: use IconButton.
- For a distribution of ratings (5 stars 82%, 4 stars 11%...). Use a Meter per row; see the summary example.
- For a bipolar or continuous attribute ("runs small ... runs large"). Use Slider.
- For a single yes/no answer with no scale. Use Checkbox or Radio.
- For content that cannot sensibly be scored, or where nobody is likely to answer. A control nobody uses is clutter; ask in a Textarea or leave it out.
Installation
pnpm add @tessinaui/uiUsage
import { Rating } from "@tessinaui/ui";{/* Star (default) */}
<Rating label="Overall rating" defaultValue={4} />
{/* Half-star */}
<Rating label="Overall rating" precision={0.5} defaultValue={3.5} />
{/* NPS numeric scale — set min={0}, since NPS runs 0–10 */}
<Rating
variant="numeric"
label="Likelihood to recommend"
min={0}
max={10}
lowLabel="Not likely"
highLabel="Extremely likely"
/>
{/* Emoji reactions */}
<Rating variant="reaction" label="How do you feel?" />
{/* Binary thumbs */}
<Rating variant="binary" label="Was this helpful?" />
{/* Read-only display of an aggregate score */}
<Rating readOnly value={4.6} />Examples
Default
The star scale, uncontrolled. label names the group for assistive
technology — always pass it, or a screen reader announces only "Star rating".
Variants
Four shapes on one control. star for review scores, numeric for NPS and CSAT
scales, reaction for quick sentiment, binary for a helpful/not-helpful answer.
They share every prop; only the anatomy differs.
Sizes
Five steps from xs to xl. The boxes run the input family's ladder, so a
md Rating lines up with a md Field in the same form row. Whatever the size
you pick, every target is at least 24px with a mouse and 44px on a touch
screen.
Intents
intent carries meaning, not decoration. Star keeps warning — amber stars are
the universal convention — and every other shape defaults to the neutral none,
which is what most real scales render.
Half-star precision
precision={0.5} lets someone pick a half. The half is real, not cosmetic:
arrows step by 0.5 and the selected star announces "3.5 stars", so the value a
screen-reader user hears is the value you receive.
A word for the value
showValue renders the readout; getValueLabel decides what it says. It is one
seam for both the visible text and the accessible value, so a descriptor like
"Very good" cannot drift from what assistive technology announces.
Anchor labels
lowLabel and highLabel pin the ends of the scale, which is what stops a
0–10 row being ambiguous about which end is good. They take any node, so an
emoji pair works as well as words.
Labels under each item
itemLabelPlacement="below" captions every step. Use it when the steps are not
self-evident — a five-point quality scale reads far better as
Poor · Fair · Good · Very good · Exceptional than as five identical stars.
NPS scale
Net Promoter Score runs 0–10, so pass min={0}: an eleven-box scale, not a
ten-box one. The row wraps rather than overflowing on a narrow screen.
Clearing the value
allowClear lets someone undo a rating — select the current value again, or
press Delete. Without it a mis-tap is permanent, which is why the
callback reports reason: "clear" separately from a normal selection.
Custom icons
icon and emptyIcon replace the star glyph. Keep both the same shape: the
filled/empty distinction is carried by solid-vs-outline, which is what makes the
scale readable without relying on colour alone.
Read-only display
A score you are showing is not a disabled input. readOnly renders one labelled
image announcing "4.6 of 5", and the stars paint the exact fraction — 4.6
fills 60% of the fifth star rather than rounding to a half. Pair max={1} with
your own number for the compact single-star form.
Summary with a breakdown
The average, the count and the per-star distribution are a composition, not
props: a read-only Rating for the average and a Meter per row for the bars.
The histogram is Meter's job, so it stays themeable and accessible on its own.
Inside a form
name renders a hidden input, so the value submits with the rest of the form;
required and errorMessage wire the invalid state to the group. Multiple
labelled rows are the standard shape for a multi-category review.
Rating, then follow-up
Asking for detail only after the score arrives is the pattern nearly every delivery and marketplace app uses — the first tap is cheap, and the follow-up is optional.
Binary as a form control
The thumbs pair is a real radiogroup with a name and a value. That is what
separates it from the icon-only thumbs in a chat toolbar, which act immediately
and belong to IconButton.
On a coloured surface
tone="on-color" is inherited from a Surface, so a rating inside a coloured
panel derives every ink from that panel's own text colour. It needs no
cooperation from the plate and is correct on a tint, a dark panel or a photo.
Right to left
Rating follows the direction it renders in: a dir on the page, on any
ancestor or on the rating itself. Right to left, the first star sits on the
right, a half star fills from its right edge, and ← moves to the
next star, matching the visual order. Pass dir="ltr" to keep one rating left
to right inside an RTL page, and a translated getValueLabel so the readout is
localised too.
Controlled
Pass value with onValueChange to own the state. The second argument reports
why the value changed — pointer, keyboard or clear — which is what you
need to tell a deliberate reset from a fresh selection.
States
Read-only shows the value, disabled takes the control out of the form, and
errorMessage marks the group invalid and describes it.
Skeleton
skeleton renders the placeholder from the component's own geometry, so the
boxes, the radius and the mobile touch floor match what loads in — including the
eleven boxes of a 0–10 scale.
Variants
Variant options
| Variant | Pattern | Use case |
|---|---|---|
star | ★★★★☆ | Product ratings, review scores |
numeric | 1 2 3 … 10 | NPS, customer satisfaction (CSAT) |
reaction | 😡 😕 😐 🙂 😄 | App feedback, quick sentiment |
binary | 👎 👍 | Helpful / not helpful |
Intent options
none (default for numeric, reaction and binary), warning (default for star),
error, success, info. primary is a deprecated alias for none.
Size options
xs, sm, md (default), lg, xl — the input family's ladder.
Rounded options
none, sm, md, lg, full. Defaults per variant: star sm, numeric md,
reaction and binary full.
API Reference
Rating Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "star" | "numeric" | "reaction" | "binary" | "star" | Visual pattern |
value | number | null | — | Controlled value (null = nothing selected) |
defaultValue | number | null | null | Uncontrolled initial value |
onValueChange | (value, { reason }) => void | — | Called when the value changes. reason is "pointer" | "keyboard" | "clear" |
max | number | 5 (star, reaction, binary) / 10 (numeric) | Highest value |
min | 0 | 1 | 1 | Numeric only — lowest value. NPS runs from 0 |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Density |
intent | "none" | "warning" | "error" | "success" | "info" | "warning" (star) / "none" (rest) | Semantic colour |
tone | "default" | "on-color" | inherited from Surface | Set on a coloured plate |
readOnly | boolean | false | Display mode — role="img", exact fractional fill, no input |
disabled | boolean | false | Form-disabled; the disabled tokens on the page, the plate's own ink dimmed on a coloured surface |
precision | 1 | 0.5 | 1 | Star only — 0.5 enables half-star selection |
allowClear | boolean | false | Re-selecting the current value (or Delete) clears it |
dir | "ltr" | "rtl" | inherited | Overrides the direction inherited from the page. Mirrors the row, the half-star fill and the horizontal arrow keys |
icon / emptyIcon | ReactNode | — | Star only — custom filled / empty glyph |
items | RatingItem[] | built-in | Reaction and binary — the option list |
itemLabelPlacement | "inline" | "below" | "hidden" | "inline" (reaction, binary) / "hidden" (star, numeric) | Where each item's label sits. Hidden labels stay in the accessibility tree |
showValue | boolean | false | Render the value readout |
valuePosition | "end" | "below" | "below" | Where the readout sits |
getValueLabel | (value, max) => string | "3 of 5" / "Not rated" | The readout text and the read-only accessible name |
getItemLabel | (value, max) => string | "3 stars" / "3 of 5" | One item's accessible name |
lowLabel / highLabel | ReactNode | — | Labels pinned to the ends of the scale |
label | string | variant default | The group's accessible name |
name | string | — | Submit the value under this name (renders a hidden input) |
required | boolean | false | Marks the group required for the form and assistive tech |
errorMessage | ReactNode | — | Error text below the control; implies an invalid state |
rounded | "none" | "sm" | "md" | "lg" | "full" | per variant | Corner radius of each item |
skeleton | boolean | false | Render the loading placeholder instead |
className | string | — | Additional classes on the root |
Deprecated for one minor, still working: onChange (use onValueChange),
showLabel (use showValue for the readout and label for the name), and
intent="primary" (use intent="none"). Each warns once in development.
RatingItem
Used by items for the reaction and binary variants.
| Field | Type | Description |
|---|---|---|
value | number | Unique option value |
label | string | The option's accessible name, and its visible label unless hidden |
icon | ReactNode | Optional glyph rendered before the label |
RatingSkeleton Props
variant, size, rounded, max, min, items, dir — the geometry props,
so the placeholder is the same box as the component. Plus label: the skeleton
is silent by default and only announces when you give it one, because a
loading region should announce once, not once per placeholder.
Accessibility
- Roles. The interactive control is a
radiogroupofradios. Exactly one radio is ever checked — including at half precision, where the checked star announces the real value ("3.5 stars").readOnlyis not a disabled radiogroup: it renders a singlerole="img"labelled with the value, so a score is announced once instead of presenting options nobody can use. - Keyboard. The group is one Tab stop on every variant. ←
→ ↑ ↓ move the value (by
precisionon a star scale), Home and End jump to the ends, and Delete / Backspace clear whenallowClearis set. Right to left, from the page or fromdir, the horizontal arrows mirror, so ← moves forward. - Naming. Pass
label.getValueLabelandgetItemLabelare the localisation seams for the value and the individual options;itemscarry their own labels. - Forms.
requiredsetsaria-required,errorMessagesetsaria-invalidand describes the group, andnamesubmits the value through a hidden input. - Colour is never the only signal. Filled and empty are different glyphs (solid vs outline), not two tints of one colour, and the empty glyph is drawn in the boundary token gated at 3:1. On a coloured surface it takes the surface's muted ink, which clears 3:1 on every surface colour in both themes.
- Targets. Every star, box and button is at least 24×24 with a mouse (WCAG 2.5.8) and 44×44 on a touch screen (2.5.5), at every size. A read-only rating is one image rather than a set of targets, so its stars keep their glyph's size.
- VoiceOver passed 2026-09-14 (Safari). Focus reads "4 stars selected radio button, 4 of 5, Star rating radio group". Arrows move the selection and read it: "5 stars selected radio button, 5 of 5", then back to "3 stars selected radio button, 3 of 5". Half-star arrows passed on 2026-09-13 (TES-192). NVDA and JAWS are untested.