# Rating (/docs/rating)





`Rating` renders a fixed-`count` row of symbols — a bundled star by default — and wires up Ark UI's RatingGroup machine: click-to-set, drag-hover preview, `←/→` / `Home` / `End` keyboard navigation, and the roving-tabindex `role="radio"` group all come for free.

```tsx
import { Rating } from '@42-central/ui-react/rating';

<Rating defaultValue={3} />
```

## Playground [#playground]

<story.Playground />

## Count [#count]

`count` sets the total number of symbols (default `5`).

<Preview code="&#x22;<Rating count={3} defaultValue={2} />\n<Rating count={10} defaultValue={6} />&#x22;">
  <div className="flex flex-col gap-3">
    <Rating count="3" defaultValue="2" />

    <Rating count="10" defaultValue="6" />
  </div>
</Preview>

## Half stars [#half-stars]

Pass `allowHalf` for half-symbol precision — a click (or an arrow key) can land on a half value. This is Ark's own half-star support only; arbitrary fractional precision is out of scope.

<story.Preview variant="Half stars" />

## Sizes [#sizes]

Five sizes — `xs` through `xl` — matching the rest of the family.

<Preview align="block" code="&#x22;<Rating size=\&#x22;xs\&#x22; defaultValue={3} />\n<Rating size=\&#x22;sm\&#x22; defaultValue={3} />\n<Rating size=\&#x22;md\&#x22; defaultValue={3} />\n<Rating size=\&#x22;lg\&#x22; defaultValue={3} />\n<Rating size=\&#x22;xl\&#x22; defaultValue={3} />&#x22;">
  <div className="flex flex-col gap-3">
    <Rating size="xs" defaultValue="3" />

    <Rating size="sm" defaultValue="3" />

    <Rating size="md" defaultValue="3" />

    <Rating size="lg" defaultValue="3" />

    <Rating size="xl" defaultValue="3" />
  </div>
</Preview>

## Colors [#colors]

The filled portion of each symbol rides the kit's `data-color` + `--c-solid` slot-var system — the same recipe as [`Slider`](/docs/slider)'s track fill. Unset, it falls back to the neutral black/white default.

<Preview align="block" code="&#x22;import { COLORS } from '@42-central/ui-react';\n\n{COLORS.map((c) => (\n  <Rating key={c} color={c} defaultValue={4} />\n))}&#x22;">
  <div className="grid gap-x-8 gap-y-3 sm:grid-cols-2">
    {COLORS.map((c) => (
          <div key={c} className="flex items-center gap-3">
            <code className="w-14 text-xs text-gray-light-500 dark:text-gray-dark-420">{c}</code>
            <Rating defaultValue={4} color={c} />
          </div>
        ))}
  </div>
</Preview>

### Per-star colors [#per-star-colors]

`color` also accepts `(index: number) => Color`, called once per symbol with its 0-based position — every star resolves its own color independently instead of sharing one for the whole group. A mood scale is the classic case: red for a poor rating, green for a great one.

<Preview code="&#x22;const moodColors: Color[] = ['red', 'orange', 'yellow', 'teal', 'green'];\n\n<Rating count={5} defaultValue={4} color={(index) => moodColors[index]} />&#x22;">
  <RatingMoodDemo />
</Preview>

## Custom symbols [#custom-symbols]

Override `fullSymbol` / `emptySymbol` to replace the bundled star with anything else — an emoji, an icon from your own set, whatever fits. Half-fill still works: whichever nodes you pass are layered and clipped the same way as the default star.

<Preview code="&#x22;<Rating\n  allowHalf\n  defaultValue={3.5}\n  fullSymbol={<span>❤️</span>}\n  emptySymbol={<span className=\&#x22;opacity-25\&#x22;>🤍</span>}\n/>&#x22;">
  <Rating defaultValue="3.5" fullSymbol="<span>❤️</span>" emptySymbol="<span className=&#x22;opacity-25&#x22;>🤍</span>" />
</Preview>

## Highlight selected only [#highlight-selected-only]

By default every symbol up to the hovered/selected value fills in — the usual "N out of `count`" look (Ark's `highlighted` state). Pass `highlightSelectedOnly` to fill just the exact selected value's symbol instead (Ark's `checked` state) — useful for a single-choice "pick a size" style row rather than a magnitude rating.

<story.Preview variant="Highlight selected only" />

## Controlled [#controlled]

`onChange` hands you the next rating value directly (a value, not a DOM event).

```tsx
'use client';
import { useState } from 'react';
import { Rating } from '@42-central/ui-react/rating';

export function CourseRating() {
  const [value, setValue] = useState(0);
  return <Rating value={value} onChange={setValue} />;
}
```

## Read-only & disabled [#read-only--disabled]

`readOnly` keeps full contrast and stays announced to assistive tech but rejects changes. `disabled` dims the control and blocks all interaction.

<Preview align="block" code="&#x22;<Rating readOnly defaultValue={4} />\n<Rating disabled defaultValue={2} />&#x22;">
  <div className="flex flex-col gap-3">
    <Rating defaultValue="4" />

    <Rating defaultValue="2" />
  </div>
</Preview>

## Field integration [#field-integration]

`Rating` has no built-in label / description / error — it's a bare control (like [`Calendar`](/docs/calendar)). Wrap it in [`Field`](/docs/field) and the label, description, error, and `aria-*` relationships wire up onto the real `role="radiogroup"` control:

```tsx
<Field label="Rate this course" description="1 star = poor, 5 stars = excellent">
  <Rating />
</Field>
```

<Preview align="block" code="&#x22;<Field label=\&#x22;Rate this course\&#x22; description=\&#x22;1 star = poor, 5 stars = excellent\&#x22;>\n  <Rating />\n</Field>&#x22;">
  <Field label="Rate this course" description="1 star = poor, 5 stars = excellent">
    <Rating />
  </Field>
</Preview>

## Forms [#forms]

`name` (and `form`) wire the group into native `<form>` submission and `FormData` via a hidden input — no controlled state required.

<Preview code="&#x22;<form action={save}>\n  <Rating name=\&#x22;rating\&#x22; defaultValue={4} />\n</form>&#x22;">
  <Rating name="rating" defaultValue="4" />
</Preview>

## API [#api]

<TypeTable
  type="{
  count: {
    type: 'number',
    default: '5',
    description: 'Total number of symbols in the group.',
  },
  value: {
    type: 'number',
    description: 'Controlled value. With allowHalf, halves (e.g. 3.5) are valid.',
  },
  defaultValue: {
    type: 'number',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(value: number) => void',
    description: 'Fires with the next rating value (a value, not a DOM event).',
  },
  allowHalf: {
    type: 'boolean',
    default: 'false',
    description: 'Half-symbol precision for pointer clicks and the keyboard step. Ark-native only — not arbitrary fractions.',
  },
  highlightSelectedOnly: {
    type: 'boolean',
    default: 'false',
    description: 'Fill only the exact selected symbol (checked) instead of every symbol up to it (highlighted).',
  },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Symbol size.' },
  color: {
    type: 'Color | ((index: number) => Color)',
    description: 'Fill color for the filled portion of each symbol, via data-color. A function is called per symbol (0-based index) for independent per-star tinting.',
  },
  fullSymbol: { type: 'ReactNode', description: &#x22;Overrides the bundled solid star for a symbol's filled portion.&#x22; },
  emptySymbol: { type: 'ReactNode', description: &#x22;Overrides the bundled outline star for a symbol's empty portion.&#x22; },
  disabled: { type: 'boolean', default: 'false', description: 'Dims and blocks interaction.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Announces the value but rejects changes.' },
  name: { type: 'string', description: 'Name for the hidden input (form submission).' },
  form: { type: 'string', description: 'The associated <form> id for the hidden input.' },
  translations: {
    type: '{ ratingValueText?: (index: number) => string }',
    description: &#x22;Partial override of each symbol's accessible label; merges over an English default.&#x22;,
  },
  classNames: {
    type: '{ root?, control?, item?, symbol? }',
    description: 'Per-part class overrides.',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the role=&#x22;radiogroup&#x22; control. Each symbol also gets its own `${testId}-item-${i}` (1-indexed). See the Testing guide.',
  },
}"
/>

All other props are forwarded to the underlying Ark `RatingGroup.Root`.

## Accessibility [#accessibility]

* Full keyboard model from Ark: `←` / `→` step by one symbol (or half, with `allowHalf`), `Home` / `End` jump to the minimum / maximum, `Space` sets the value when none is set yet.
* Each symbol is `role="radio"` in a `role="radiogroup"`, with a roving tabindex — `Tab` reaches the group once, arrows move within it.
* `aria-label` on each symbol names its value (`translations.ratingValueText`); `aria-checked` / `aria-posinset` / `aria-setsize` are wired by Ark.
* A hidden `<input>` backs form submission — pass `name` to include it.
* RTL is honored via the document `dir`: hover/click position and the half-symbol clip both mirror automatically.
