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.
Playground
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} />When to use which
Rating is a form control: it holds a value, it can be submitted, and it is operable from the keyboard. Three neighbouring patterns look similar and are not this component.
| You want | Use |
|---|---|
| A score the person picks and you submit | Rating |
| A score you are only displaying | Rating with readOnly — it becomes a labelled image, not a set of unusable radios |
| Thumbs in a chat or AI message toolbar | IconButton — those act immediately, hold no value, and belong in the message's action row |
| A distribution of ratings (5★ 82%, 4★ 11%…) | Meter per row — see the summary example |
| A bipolar attribute scale ("runs small … runs large") | Slider |
| A single yes/no answer with no scale | Checkbox or Radio |
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. On touch viewports
every target floors at 44px regardless of the size you pick.
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
dir="rtl" mirrors the row and the keyboard: ← moves forward,
matching the visual order. Pass 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" | "ltr" | Mirrors the row 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. Underdir="rtl"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.
- Touch. Every target floors at 44×44 on touch viewports (WCAG 2.5.5) while desktop keeps the compact density.
- Tested with VoiceOver on macOS. NVDA and JAWS are untested.