Rating
Part of the extras bundle, not included by default.
Intent
A star rating in interactive and read-only flavors: a radio
group for capturing a value, and a display-only form for
showing one.
The visual fill of stars is driven by CSS only --
:has() handles selection and hover trails,
mask-image clips a colored block to the star
silhouette, and a single token swaps the silhouette to any
SVG path (heart, thumb, custom shape).
No JS. The interactive form is real <input
type="radio"> elements with native keyboard
behavior; brio just paints the chrome on top.
Basic usage
Wrap five radio inputs in .rating in ascending
DOM order (1 → 5). Each input is paired with a label that
contains .visually-hidden text describing the
value -- screen readers hear "1 star", "2 stars", etc.; sighted
users see only the star shape painted by the CSS mask.
<div class="rating" role="radiogroup" aria-label="Rate this">
<input type="radio" id="r1-1" name="rating-1" value="1"/>
<label for="r1-1"><span class="visually-hidden">1 star</span></label>
<input type="radio" id="r1-2" name="rating-1" value="2"/>
<label for="r1-2"><span class="visually-hidden">2 stars</span></label>
<input type="radio" id="r1-3" name="rating-1" value="3"/>
<label for="r1-3"><span class="visually-hidden">3 stars</span></label>
<input type="radio" id="r1-4" name="rating-1" value="4"/>
<label for="r1-4"><span class="visually-hidden">4 stars</span></label>
<input type="radio" id="r1-5" name="rating-1" value="5"/>
<label for="r1-5"><span class="visually-hidden">5 stars</span></label>
</div>Click or tap a star to select. Hovering shows a preview -- the stars from the hovered position back to the start fill while the cursor is over the rating; release to either keep or change the previous selection. Tab into the rating to focus the selected (or first) radio; arrow keys walk between values.
Pre-selected value
Add checked to the input matching the initial
value -- that radio's adjacent label and every earlier label
fill via the :has() selector chain.
<div class="rating" role="radiogroup" aria-label="Rate this">
<input type="radio" id="r2-1" name="rating-2" value="1"/>
<label for="r2-1"><span class="visually-hidden">1 star</span></label>
<input type="radio" id="r2-2" name="rating-2" value="2"/>
<label for="r2-2"><span class="visually-hidden">2 stars</span></label>
<input type="radio" id="r2-3" name="rating-2" value="3" checked=""/>
<label for="r2-3"><span class="visually-hidden">3 stars</span></label>
<input type="radio" id="r2-4" name="rating-2" value="4"/>
<label for="r2-4"><span class="visually-hidden">4 stars</span></label>
<input type="radio" id="r2-5" name="rating-2" value="5"/>
<label for="r2-5"><span class="visually-hidden">5 stars</span></label>
</div>Read-only display
For showing a fixed rating without an editable input, use
.rating.is-readonly with
<span class="star"> children. Mark filled
positions with .is-filled. The container takes
role="img" with a descriptive
aria-label -- the visual stars are decorative;
the label carries the value.
<div class="rating is-readonly" role="img" aria-label="Rated 4 out of 5 stars">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>Sizes
Three tiers via --rating-size overrides:
.is-sm (1rem), default (1.5rem),
.is-lg (2rem). Override
--rating-size directly for one-offs.
<div class="stack">
<div class="rating is-sm is-readonly" role="img" aria-label="Rated 4 out of 5 stars">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>
<div class="rating is-readonly" role="img" aria-label="Rated 4 out of 5 stars">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>
<div class="rating is-lg is-readonly" role="img" aria-label="Rated 4 out of 5 stars">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>
</div>Custom icon
Override --rating-icon with any single-path SVG
data-URI. The CSS mask clips the colored fill to the new
silhouette -- no markup change. Pair with a different
--rating-active for thematic shapes (red hearts,
green thumbs).
<div class="rating is-readonly" role="img" aria-label="Rated 4 out of 5" style="--rating-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/%3E%3C/svg%3E"); --rating-active: var(--color-danger);">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>Custom colors
Override --rating-active for non-default fill
colors -- success-green for "user satisfied" surveys,
primary-tinted for brand-aligned ratings.
<div class="stack">
<div class="rating is-readonly" role="img" aria-label="Rated 3 out of 5 stars" style="--rating-active: var(--color-success);">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
<span class="star"></span>
</div>
<div class="rating is-readonly" role="img" aria-label="Rated 4 out of 5 stars" style="--rating-active: var(--color-primary);">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>
</div>How it works
Inputs are positioned absolutely with zero size and zero opacity -- visually gone but still in the tab order, so keyboard navigation through the radio group works the way users expect (Tab into the group, arrow keys to move).
Each label is a colored block whose silhouette is clipped by
a mask-image (the SVG star). The label's
background color is the only thing the user sees through the
mask. Selection and hover swap the background color; the
silhouette never changes.
The selection / hover logic uses two mutually-exclusive
:has() branches -- when a label is hovered,
selection deactivates automatically; when no label is hovered,
selection takes over. No cascade fight, no specificity tuning.
-
Selection
(
.rating:not(:has(label:hover))guard).input:checked + labelfills the chosen star;label:has(~ input:checked)fills every label whose later sibling is the checked input. The:not(:has(label:hover))guard keeps selection visible when the cursor is in the gap between stars -- only deactivates when a real label is hovered. -
Hover trail.
label:hoverfills the hovered star;label:has(~ label:hover)fills every earlier label. The selection rule's guard goes false the moment any label is hovered, so the hover trail is the only fill in play -- no specificity competition with the selection rule. Releasing hover flips the guard back to true and selection re-takes over.
Customization
Scoped tokens drive the rating:
--rating-size (axis size of each star),
--rating-icon (the SVG data-URI silhouette),
--rating-color (empty fill, defaults to
--color-border),
--rating-active (filled color, defaults to
--color-warning), and --rating-gap
(between stars). Override any combination on the root for
instance-specific theming.
Accessibility
-
Interactive ratings use
role="radiogroup"with a descriptivearia-label. Each label's.visually-hiddentext gives the radio its accessible name ("3 stars"). - Keyboard navigation works via native radio behavior -- Tab into the group, arrow keys to move between values, Space to select. The visible focus ring on the label next to the focused input shows where focus currently lives.
-
Read-only ratings use
role="img"with anaria-labelthat describes the value ("Rated 4 out of 5 stars"). The individual.starelements are decorative; the label carries the meaning. -
Color isn't sufficient on its own to convey the value --
users with color-vision differences or screen readers
rely on the label text. Always include the
aria-labelor.visually-hiddentext.
Markup
<!-- Interactive -->
<div class="rating" role="radiogroup" aria-label="Rate this">
<input type="radio" id="r1" name="rating" value="1">
<label for="r1"><span class="visually-hidden">1 star</span></label>
<input type="radio" id="r2" name="rating" value="2">
<label for="r2"><span class="visually-hidden">2 stars</span></label>
<input type="radio" id="r3" name="rating" value="3" checked>
<label for="r3"><span class="visually-hidden">3 stars</span></label>
<input type="radio" id="r4" name="rating" value="4">
<label for="r4"><span class="visually-hidden">4 stars</span></label>
<input type="radio" id="r5" name="rating" value="5">
<label for="r5"><span class="visually-hidden">5 stars</span></label>
</div>
<!-- Read-only -->
<div class="rating is-readonly" role="img" aria-label="Rated 4 out of 5 stars">
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star is-filled"></span>
<span class="star"></span>
</div>
CSS Reference: Rating
Star rating with CSS-only interaction
Source: src/css/extras/rating.css
Tokens
| Token | Default | Description |
|---|---|---|
--rating-size | 1.5rem | Inline and block size of each star |
--rating-icon | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01z'/%3E%3C/svg%3E") | Mask image for the star shape, swappable to any single-path SVG |
--rating-color | var(--color-border) | Color of unselected stars |
--rating-active | var(--color-warning) | Color of selected and hovered stars |
--rating-gap | var(--gap-compact) | Gap between stars |
Classes
| Class | Description |
|---|---|
.rating | component root |
.is-readonly | Display-only mode using span elements instead of radio inputs |
.is-sm | Small size variant (1rem stars) |
.is-lg | Large size variant (2rem stars) |