Rating
A star/score control for collecting or displaying a rating.
@garn/ui/ratingOn this page
Default
A five-star rating with an accessible name.
Required
Required in a form — no clear anchor, native validation, submits its value.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add rating- Registry
rating- Deps
class-variance-authoritylucide-react- Registry deps
utilsuse-controllable-state
Import
import { Rating } from "@garn/ui/rating";Props
The component's public props and their types.
size- Description
- No description.
- Type
"lg" | "md" | "sm"- Default
md
tone- Description
- No description.
- Type
"brand" | "danger" | "discovery" | "info" | "neutral" | "success" | "warning"- Default
warning
clearable- Description
- Re-selecting the current value resets it to 0 (ignored when `required`).
- Type
boolean- Default
true
defaultValue- Description
- Uncontrolled initial rating.
- Type
number- Default
0
disabled- Description
- Keep the radios but make them inert — stays in the form/accessibility tree, greyed out.
- Type
boolean- Default
false
emptyIcon- Description
- Symbol shown for empty positions; falls back to `icon` when omitted.
- Type
React.ReactNode
getValueText- Description
- Spoken label for a value — defaults to `"{value} of {max}"`.
- Type
(value: number, max: number) => string
icon- Description
- Filled symbol; also the empty symbol unless `emptyIcon` is given.
- Type
React.ReactNode
max- Description
- Number of symbols.
- Type
number- Default
5
name- Description
- Groups the radios and names the form field.
- Type
string
onHoverChange- Description
- Fires with the previewed value on hover, `null` on leave.
- Type
(value: number | null) => void
onValueChange- Description
- Fires with the committed rating; `0` when cleared.
- Type
(value: number) => void
precision- Description
- Selection granularity — whole or half symbols.
- Type
0.5 | 1- Default
1
readOnly- Description
- Render-only: no inputs, `role="img"`, fractional fill for averages.
- Type
boolean- Default
false
required- Description
- Form validation — a value must be picked; implies not clearable.
- Type
boolean- Default
false
value- Description
- Controlled rating (0…max, in `precision` steps).
- Type
number
Plus 277 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="rating"]States
| Selector | State | Description |
|---|---|---|
:focus-visible | focus-visible | A keyboard-focused radio rings its whole symbol (via `has-[:focus-visible]`). |
:hover | hover-preview | Pointer hover fills up to the hovered symbol and reverts on leave (mouse only — touch taps commit without a stuck preview). |
[data-disabled] | disabled | Dims and blocks interaction; radios stay in the form tree, disabled. |
[data-readonly] | read-only | Static display — role=img, fractional fill, no radios. |
| Property | Token | Tier |
|---|---|---|
fill | --garn-badge-warning-boldbrandable | semantic |
ring | --garn-ringbrandable | semantic |
symbolSize | --garn-box-lg | semantic · identity |
Private instance vars (never externalize): --r-fill--r-size
When to use
Collect or display a score on a small fixed scale — a star review, a helpfulness vote, a difficulty rating — where cumulative symbols read faster than a number.
Reach for something else when
- A precise value on a large scale (use a number input)
- an approximate position on a continuum (use slider)
- a single on/off reaction (use toggle or switch).
Overview
Rating collects or shows a score on a small fixed scale — the five-star review, the thumbs-worth of a helpfulness vote, a difficulty out of ten. It is one component in two modes: interactively it's a radiogroup of real (hidden) radios, so keyboard, focus and form submission are native; read-only it's an image that announces a fractional average and clips the fill to any decimal. Symbols fill cumulatively up to the value, with a live hover-preview that reverts on leave. When the scale is large or the exact number matters, a number field or slider is clearer; for a single yes/no reaction, use a Toggle.
Guidelines
Name the control. An interactive rating is a radiogroup and needs an accessible name — pass aria-label ("Rate this product") or aria-labelledby. It warns in dev when unnamed.
Pick the right mode. Collecting input → interactive (keep it clearable so people can un-rate, or set required in a form to force a choice). Showing an aggregate like 4.3★ → readOnly with precision={0.5}; read-only clips the fill to the exact decimal and announces "4.3 of 5".
Surface the meaning. Pair the stars with a count or a word scale ("Terrible…Excellent") driven by onHoverChange, and keep max small (5 is the convention) — a 10- or 100-point scale is better as a number field.
Tone is intent, not decoration. Amber (warning) is the default star; use another tone only when it carries meaning (a red heart for favourites). Empty symbols stay neutral grey by design.
Best practices
- Give the interactive rating an accessible name (aria-label / aria-labelledby).
- Use
readOnly+precision={0.5}to display fractional averages like 4.3. - Keep it clearable for optional input, or
requiredto force a choice in a form. - Show a count or word-scale label alongside, driven by
onHoverChange.
- Don't leave an interactive rating unlabeled.
- Don't use a rating for a large or precise scale — reach for a number input.
- Don't recolour the symbols for decoration; tone should carry meaning.
Content guidelines
- Name what's being rated in the label ("Rate this product", not "Rating").
- Show the numeric score and review count next to read-only averages.
- Use a consistent word scale if you label hover values (Terrible → Excellent).
Troubleshooting
Type error or nothing selected when passing an array to value.
Cause. Rating's value is a single number, not an array like Slider.
Fix. Pass a number — value={3} / defaultValue={2.5}.
Console warns that the rating has no accessible name.
Cause. An interactive rating is a radiogroup and needs a name.
Fix. Add aria-label or aria-labelledby (read-only mode names itself from the score).
A fractional value like 4.3 renders as a whole/half star only.
Cause. Fractional fill is a read-only display concern; interactive selection snaps to precision.
Fix. Use readOnly for averages (any decimal fills exactly); precision={0.5} only affects what a user can pick.
required never blocks form submission.
Cause. The default clear anchor keeps a value (0) checked, satisfying required.
Fix. Set required — it drops the clear anchor so an unrated field is invalid (and implies not-clearable).
Accessibility
- Role
radiogroup- ARIA APG
- radiogroup
- Focus
- A focused radio rings its whole symbol.
Accessibility requirements
Give the interactive rating an accessible name (aria-label or aria-labelledby). Warns in dev.
when an interactive Rating has no aria-label / aria-labelledby
Keyboard
| ArrowRightArrowUp | Select the next value (native radio behaviour). |
| ArrowLeftArrowDown | Select the previous value. |
| Home | Jump to the lowest value. |
| End | Jump to `max`. |
| PageUpPageDown | Nudge by one whole symbol. |
- Interactive: role=radiogroup built from real (visually-hidden) radios — give it an accessible name via aria-label or aria-labelledby.
- Each radio is named for its value (e.g. "3 of 5"); a 'No rating' radio anchors the empty state (omitted when `required`).
- Read-only: role=img with an aria-label announcing the score (e.g. "4.3 of 5").