Rating
The <x-wirekit:: 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
Preselected Value
Clearable
A rating used as a filter facet or an optional field needs a way back to "no
opinion" — otherwise one mis-click sends the server a score nobody meant to
give. clearable makes the star that is already chosen a toggle, and lets
ArrowLeft step below the first star to 0.
It is off by default on purpose. Without it the control follows the ARIA radiogroup model exactly, where a chosen option cannot be un-chosen — which is right for "how many stars would you give this?" and wrong for "filter by rating".
Custom Max
Size Variants
Readonly
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 that emitted a hidden
input would, on a page showing 24 product ratings, ship 24 stray form fields that a
surrounding <form> submits.
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:
<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.
Disabled
A disabled rating is still the control, only out of use. Its stars stay on the page, dimmed, and the group stays one tab stop, so a keyboard reader still finds it and hears that it is unavailable. No click and no key changes the score, and like a disabled native field it is left out of the data a form submits.
A score that is only shown and never chosen is readonly instead. When both are set,
readonly wins.
Fractional Stars
Readonly mode supports fractional values for displaying average ratings:
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.
Fractional Icons
Custom icons also support fractional fill in readonly mode:
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:
You can also reference other design tokens instead of hardcoded hex values:
{{-- Use the accent color (indigo by default) --}}
<x-wirekit::rating label="Product rating" name="r" :value="4" style="--color-wk-warning: var(--color-wk-accent);" />
{{-- Use the success color (green) --}}
<x-wirekit::rating label="Product 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 label="Product 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:
// In a service provider's boot() method. A closure receives the shipped classes, so the
// component keeps them; a plain string would replace them outright.
WireKit::scope('love-meter', [
'rating' => ['classes' => [
// A token rather than a hex value, so the color follows the theme into dark mode.
'base' => fn (string $vendor): string => $vendor.' [--color-wk-warning:var(--color-wk-danger)]',
]],
]);
<x-wirekit::rating label="How much you love it" 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. A
disabled rating submits nothing, and a readonly one renders no field.
Livewire Integration
<x-wirekit:: 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:
<x-wirekit::rating label="Your 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.
Bound to a property declared int, float or an enum backed by int, the rating is read as a
number: wire:model.live goes out as wire:model.. Its hidden input holds the score
as text, and Livewire would take the server's typed answer to that text for a change made on the
server and write it over a score chosen while the request was out. A property without a type,
or a string one, gets the text.
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:
<x-wirekit::rating
name="score"
label="Score"
:value="$score"
optimistic="rate"
/>
Load wirekit-optimistic. alongside whichever bundle you already use — below it, in your layout:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></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.
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.
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.
Pick a rating before submitting.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
required |
bool | false |
Marks the rating as required. Emits aria-required="true" on the role="radiogroup" and renders the label indicator (*). In a form, an empty required rating stops the submit as a native required field does: the browser's own message shows under the stars, in the reader's language, and the focus moves to them when it is the first empty field. |
optimistic |
string|null |
null |
Livewire method to call, showing the new score before the server confirms it. Ignored when readonly or disabled. See 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 |
help |
string|null |
null |
An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label |
name |
string|null |
null |
Form field name |
id |
string|null |
auto-generated | The id of the field that carries the value. The stars are a radio group, which a <label for> cannot name: use label, aria-label or aria-labelledby |
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'. Every star of an interactive rating is at least a 24px target: at 'sm' the 16px star sits in the middle of a 24px button, so the small stars stand a little apart. |
clearable |
bool |
false |
Let the reader take the score back. Clicking the star that is already chosen returns the value to 0, and ArrowLeft / ArrowDown steps below one star instead of stopping there. Off by default, because a radiogroup has no way back to "nothing chosen" and that is the model this control follows — turn it on for a filter facet or an optional field, where "no opinion" has to survive a mis-click |
readonly |
bool |
false |
Display-only mode (a record of a score, not an interactive control) |
disabled |
bool |
false |
Takes the rating out of use: dimmed, announced as unavailable, no click or key changes the score, and nothing is submitted. Ignored when readonly |
scope |
string|null |
null |
Scoped personalization key |
announceError |
bool|null | null |
Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region. |
Slots
| Slot | Description |
|---|---|
labelAfter |
Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label. |
Accessibility
- Interactive container:
role="radiogroup", named by thelabelprop — or by an explicitaria-label, which always wins - Each star: a
<button role="radio">named after its own position ("3 stars"), so the reader hears the score they are about to choose - Selected state:
aria-checked="true"on exactly one star — the one equal to the current score. A radiogroup is single-select, so the stars below the score are lit but not checked: the fill is paint, the checked state is the answer - Roving tabindex: only the selected star is in the tab order (the first one, while no score is chosen), and the arrow keys move within the group
- An
autofocuswritten on the tag goes to the star that holds the tab stop - Star SVGs are
aria-hidden="true"— the button provides the accessible name - With
clearable, the zero state is reachable by keyboard as well as by pointer: ArrowLeft / ArrowDown from one star clears the score, and no star reportsaria-checked="true"— which is the state a rating already renders before anyone has answered it. Focus does not move on that step, so the reader keeps the star they were on. Withoutclearablethe control follows the radiogroup model exactly, where a chosen option cannot be un-chosen from the keyboard - Hover preview shows which rating would be selected (visual-only, no AT announcement)
- Readonly mode is not a control at all:
role="img"named with the score itself ("4.2 out of 5 stars"), no radios, nothing focusable, and no form field - Disabled:
aria-disabled="true"on the group and on each star. The selected star stays the one tab stop, because a natively disabled group would leave the tab order and a keyboard reader would never learn the rating is there - With
optimistic, an assertive live region announces the pending state and, on a rollback, that the change was undone
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). With clearable, stepping down from one star clears the score to 0 instead of stopping there |
| 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
readonlyif 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/:
'components' => [
'rating' => ['max' => 5, 'size' => 'md'],
],
Further Reading
- WAI-ARIA: Radiogroup Pattern
- MDN:
role="radiogroup" - MDN:
role="radio" - MDN:
role="img"— whatreadonlyrenders - MDN: Roving tabindex