---
title: Rating
description: Star rating input
visibility: guest
draft: false
---

# Rating

The `<x-wirekit::rating>` component displays an interactive rating control. It uses `role="radiogroup"` with individual radio buttons for each icon, providing full keyboard and screen reader support. The default icon is a star, but you can choose from several built-in shapes.

## Basic Usage

:::preview{title="Default 5-Star Rating"}
<x-wirekit::rating label="Your rating" name="rating" />
:::

## Preselected Value

:::preview{title="3 Out of 5"}
<x-wirekit::rating label="Product rating" name="product" :value="3" />
:::

## Custom Max

:::preview{title="10-Star Scale"}
<x-wirekit::rating label="Score" name="score" :max="10" :value="7" />
:::

## Size Variants

:::preview{title="Three Sizes"}
<x-wirekit::rating label="Small" name="r-sm" size="sm" :value="4" />

<x-wirekit::rating label="Medium" name="r-md" size="md" :value="4" />

<x-wirekit::rating label="Large" name="r-lg" size="lg" :value="4" />
:::

## Readonly

:::preview{title="Display Only"}
<x-wirekit::rating label="Average rating" name="avg" :value="4" :readonly="true" />
:::

In readonly mode the rating is a **record of a score**, not a control that happens
to be switched off — so it renders as a picture: `role="img"` with one accessible
name ("4 out of 5 stars"), nothing focusable inside it, and **no form field**.

That last part matters more than it sounds. A display-only rating used to emit a
hidden input with a generated name, so a page showing 24 product ratings shipped 24
stray form fields that a surrounding `<form>` would submit.

Pass `aria-label` when "4 out of 5 stars" does not say enough on its own — with
several ratings on a page, none of them says *what* scored 4:

```blade
<x-wirekit::rating :value="4.2" readonly aria-label="Average customer rating" />
```

An interactive rating is unchanged: it is a real `radiogroup`, because picking a
score *is* choosing one of five.

## Fractional Stars

Readonly mode supports fractional values for displaying average ratings:

:::preview{title="Fractional Ratings"}
<x-wirekit::rating label="3.78 average" name="frac1" :value="3.78" :readonly="true" />

<x-wirekit::rating label="4.5 stars" name="frac2" :value="4.5" :readonly="true" />

<x-wirekit::rating label="2.2 stars" name="frac3" :value="2.2" :readonly="true" />
:::

Fractional values only work in `readonly` mode. Interactive ratings always snap to whole icons.

## Icon Shapes

The `icon` prop lets you swap stars for other shapes. All shapes support interactive mode, readonly mode, fractional fill, and all size variants.

:::preview{title="Available icon shapes"}
<x-wirekit::stack gap="md">
    <x-wirekit::rating label="Star (default)" name="i-star" icon="star" :value="4" />
    <x-wirekit::rating label="Heart" name="i-heart" icon="heart" :value="4" />
    <x-wirekit::rating label="Circle" name="i-circle" icon="circle" :value="4" />
    <x-wirekit::rating label="Square" name="i-square" icon="square" :value="4" />
    <x-wirekit::rating label="Diamond" name="i-diamond" icon="diamond" :value="4" />
    <x-wirekit::rating label="Thumb" name="i-thumb" icon="thumb" :value="4" />
</x-wirekit::stack>
:::

### Fractional Icons

Custom icons also support fractional fill in readonly mode:

:::preview{title="Fractional hearts"}
<x-wirekit::rating label="Love score" name="love" icon="heart" :value="3.7" :readonly="true" />
:::

## Custom Colors

The filled icon color uses the `--color-wk-warning` token because amber/yellow is the universal standard color for star ratings (Amazon, Google, IMDb, etc.). The token's name refers to its semantic role in the design system (warning notifications), but the amber hue happens to be the perfect match for rating icons.

You can recolor ratings per instance by overriding `--color-wk-warning` with any value — including other WireKit tokens:

:::preview{title="Custom rating colors"}
<x-wirekit::stack gap="md">
    <x-wirekit::rating label="Default (amber)" name="c-default" :value="4" :readonly="true" />
    <x-wirekit::rating label="Red hearts" name="c-red" icon="heart" :value="4" :readonly="true" style="--color-wk-warning: #e11d48;" />
    <x-wirekit::rating label="Blue stars" name="c-blue" :value="4" :readonly="true" style="--color-wk-warning: #2563eb;" />
    <x-wirekit::rating label="Green circles" name="c-green" icon="circle" :value="4" :readonly="true" style="--color-wk-warning: #16a34a;" />
</x-wirekit::stack>
:::

You can also reference other design tokens instead of hardcoded hex values:

```blade
{{-- Use the accent color (indigo by default) --}}
<x-wirekit::rating name="r" :value="4" style="--color-wk-warning: var(--color-wk-accent);" />

{{-- Use the success color (green) --}}
<x-wirekit::rating name="r" icon="circle" :value="3" style="--color-wk-warning: var(--color-wk-success);" />

{{-- Use the danger color (red) for hearts --}}
<x-wirekit::rating name="r" icon="heart" :value="5" style="--color-wk-warning: var(--color-wk-danger);" />
```

This way your ratings stay consistent with your theme — when you change your accent or success color, the ratings update automatically.

To change the color globally, override `--color-wk-warning` in your theme. For per-scope customization, use the `scope` prop with personalization:

```php
// config/wirekit.php — personalize a specific scope
'personalizations' => [
    'rating' => [
        'love-meter' => [
            'base' => '[--color-wk-warning:#e11d48]',
        ],
    ],
],
```

```blade
<x-wirekit::rating scope="love-meter" icon="heart" name="love" :value="4" />
```

## Width & Layout

The rating component auto-sizes to its number of icons and the `size` prop. It does not stretch to fill its container — it has a natural inline width based on `max × icon size`.

## Form Submission

A hidden `<input type="hidden">` submits the numeric rating value under the given `name`.

## Livewire Integration

`<x-wirekit::rating>` keeps its current rating in internal state seeded from the
`value` prop, so when binding with Livewire pass the bound property as `:value`
**alongside** `wire:model` to seed the initial rating:

```blade
<x-wirekit::rating wire:model.live="score" :value="$score" />
```

Without `:value` the control shows `0` stars until the user picks a rating;
`wire:model` keeps it in sync afterward. This is the framework-agnostic seeding
pattern WireKit's stateful controls share — it works in plain Blade forms too.

## Optimistic UI

Pass the name of the Livewire method the rating should call and the new score appears immediately, then confirms or undoes itself when the server answers:

```blade
<x-wirekit::rating
    name="score"
    label="Score"
    :value="$score"
    optimistic="rate"
/>
```

Load `wirekit-optimistic.js` alongside whichever bundle you already use — below it, in your layout:

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
```

### Try it

The demo below runs the real path: the change shows immediately, the outline says it is
provisional, and the server's answer either confirms it silently or takes it back.

:::preview{title="Optimistic rating — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::rating name="opt-accept" label="Accepted — the confirmation is silent" :value="3" optimistic="demoAccept" />
    <x-wirekit::rating name="opt-reject" label="Refused — the old score comes back, and the refusal is spoken" :value="3" optimistic="demoReject" />
    <x-wirekit::rating name="opt-slow" label="Slow answer — the dashed outline is the provisional state" :value="3" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::rating
    name="score"
    label="Score"
    :value="$score"
    optimistic="rate"
/>
:::

The `<livewire:demos.…>` wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.

The prop takes a method name rather than `true` because the component cannot know the action otherwise: server actions reach a WireKit component through the attribute bag, so it never sees your `wire:click`. **`optimistic` replaces `wire:model.live` here rather than joining it** — the method you name is what changes the score on the server; pass the current one with `:value` so the rating knows where it started.

::: info What a screen reader hears
Picking a score announces once, hedged — "Saving" — so the new value is audible as provisional. **Confirmation is silent**: what was announced is what happened. Only a deviation speaks a second time, which is what makes an undo recognizable as an undo.

Where the rating sits in a form that already shows a validation message, the undo stays **silent** and leaves that message to speak: it tells you what to do, "could not save" does not.

An aborted request announces nothing at all — nothing was refused.

Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.
:::

A second pick while one is in flight is **refused** rather than queued. Queued, the final score would depend on which answer arrived last — network timing, which is both wrong and impossible to test.

The hidden form field follows both the optimistic write and the undo, so a plain HTML form never submits a score that was taken back.

**Readonly ratings ignore the prop.** A readonly rating is a picture of a score with nothing to operate, so there is no action to anticipate.

**Scope.** This covers mutations: changing something and showing it right away. Sorting, filtering and pagination are query round trips — nobody can show a result nobody knows yet — and are deliberately not covered.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `optimistic` | `string\|null` | `null` | Livewire method to call, showing the new score before the server confirms it. Ignored when `readonly`. See [Optimistic UI](#optimistic-ui). |
| `optimisticArgs` | `array` | `[]` | Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
| `label` | `string\|null` | `null` | Accessible group label |
| `error` | `string\|null` | `null` | Validation message. Rendered below the control, announced politely, and wired with `aria-invalid` + `aria-describedby` |
| `hint` | `string\|null` | `null` | Helper text below the control. An `error` replaces it — one message region, so the two cannot stack |
| `name` | `string\|null` | `null` | Form field name |
| `id` | `string\|null` | auto-generated | Base element id |
| `value` | `int\|float` | `0` | Rating value. Supports fractional values (e.g. `3.78`) in readonly mode |
| `max` | `int` | `5` | Maximum number of icons |
| `icon` | `string` | `'star'` | Icon shape: `star`, `heart`, `circle`, `square`, `diamond`, `thumb` |
| `size` | `string` | `'md'` | `'sm'`, `'md'`, `'lg'` |
| `readonly` | `bool` | `false` | Display-only mode (a record of a score, not an interactive control) |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## Accessibility

- Outer container: `role="radiogroup"` with `aria-label` from the `label` prop
- Each star: visually hidden `<input type="radio">` with `aria-label="N of M stars"`
- Selected state communicated via native radio `checked` attribute
- Star SVGs are `aria-hidden="true"` — the radio input provides the accessible name
- Hover preview shows which rating would be selected (visual-only, no AT announcement)
- Readonly mode sets `aria-readonly="true"` on the radiogroup
- Live region announces the selected value on change: "N of M stars"

## Keyboard Interaction

| Key | Action |
| --- | --- |
| Tab | Move focus into the rating group |
| ArrowRight / ArrowUp | Select the next star (increase the rating) |
| ArrowLeft / ArrowDown | Select the previous star (decrease the rating) |
| Home | Select the first star (1) |
| End | Select the last star (max) |

The component uses roving `tabindex` — only the currently selected star (or the first star if none selected) is in the tab order.

## Pitfalls

- **Don't use rating as a single-value display.** Reach for stars with `readonly` if the value is read-only — but for narrative copy "Rated 4.2/5" is more screen-reader-friendly than a five-star widget.

## Design Tokens

| Token | Purpose |
| --- | --- |
| `--color-wk-warning` | Filled icon color (amber by default) |
| `--color-wk-text-subtle` | Empty icon stroke color |
| `--color-wk-ring` | Focus ring color |
| `--ring-wk-width` | Focus ring width |
| `--radius-wk-sm` | Focus ring border radius |
| `--transition-wk-duration` | Hover/selection transition |
| `--font-wk-sans` | Font family for labels |

## Customization

Override defaults in `config/wirekit.php`:

```php
'components' => [
    'rating' => ['max' => 5, 'size' => 'md'],
],
```

## Further Reading

- [WAI-ARIA: Radiogroup Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/)
- [MDN: `role="radiogroup"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/radiogroup_role)
- [MDN: `role="radio"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/radio_role)
- [MDN: `aria-readonly`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-readonly)
- [MDN: Roving tabindex](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex)
