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(&#34;data:image/svg+xml,%3Csvg xmlns=&#39;http://www.w3.org/2000/svg&#39; viewBox=&#39;0 0 24 24&#39;%3E%3Cpath d=&#39;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&#39;/%3E%3C/svg%3E&#34;); --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 + label fills 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:hover fills 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 descriptive aria-label. Each label's .visually-hidden text 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 an aria-label that describes the value ("Rated 4 out of 5 stars"). The individual .star elements 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-label or .visually-hidden text.

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

TokenDefaultDescription
--rating-size1.5remInline and block size of each star
--rating-iconurl("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-colorvar(--color-border)Color of unselected stars
--rating-activevar(--color-warning)Color of selected and hovered stars
--rating-gapvar(--gap-compact)Gap between stars

Classes

ClassDescription
.ratingcomponent root
.is-readonlyDisplay-only mode using span elements instead of radio inputs
.is-smSmall size variant (1rem stars)
.is-lgLarge size variant (2rem stars)