Skip to content

Rating

A .re-rating is a <fieldset> of visually-hidden radios (one per value) paired with star <label>s — so keyboard selection, single-choice, and form value are all native, with no JavaScript. Add a value-0 radio labelled “No rating” to allow clearing. CSS-only.

The fieldset is direction: rtl as an implementation detail: the radios are in DOM order high→low so the floor-safe sibling selectors can fill the chosen star and all lower ones, and rtl flips both the visual order (to 1→5 left-to-right) and the arrow keys back into alignment (Right/Up select higher, Left/Down lower). Stars stay a low→high-left-to-right scale regardless of page direction.

Native arrow-key direction in a reversed radio group varies by browser (Chromium honors rtl, WebKit doesn’t). For consistent arrows everywhere, opt into the tiny enhanceRating behavior — it intercepts the arrow keys so Right/Up always raise and Left/Down always lower, identically across browsers. The CSS-only base stays fully usable without it.

import { enhanceRating } from "@relements/core/behaviors/rating";
enhanceRating(document);
Live example
<fieldset class="re-rating" aria-label="Rate your experience">
  <input class="re-sr-only" type="radio" name="rating" id="rate-5" value="5" />
  <label class="re-rating__star" for="rate-5" aria-label="5 stars">★</label>
  <input class="re-sr-only" type="radio" name="rating" id="rate-4" value="4" />
  <label class="re-rating__star" for="rate-4" aria-label="4 stars">★</label>
  <input class="re-sr-only" type="radio" name="rating" id="rate-3" value="3" />
  <label class="re-rating__star" for="rate-3" aria-label="3 stars">★</label>
  <input class="re-sr-only" type="radio" name="rating" id="rate-2" value="2" />
  <label class="re-rating__star" for="rate-2" aria-label="2 stars">★</label>
  <input class="re-sr-only" type="radio" name="rating" id="rate-1" value="1" />
  <label class="re-rating__star" for="rate-1" aria-label="1 star">★</label>
  <input class="re-sr-only" type="radio" name="rating" id="rate-0" value="0" checked />
  <label class="re-sr-only" for="rate-0">No rating</label>
</fieldset>

data-size="sm" or "lg" on the fieldset.

Live example
<fieldset class="re-rating" data-size="sm" aria-label="Small rating">
  <input class="re-sr-only" type="radio" name="r-sm" id="sm-3" value="3" checked />
  <label class="re-rating__star" for="sm-3" aria-label="3 stars">★</label>
  <input class="re-sr-only" type="radio" name="r-sm" id="sm-2" value="2" />
  <label class="re-rating__star" for="sm-2" aria-label="2 stars">★</label>
  <input class="re-sr-only" type="radio" name="r-sm" id="sm-1" value="1" />
  <label class="re-rating__star" for="sm-1" aria-label="1 star">★</label>
</fieldset>
<fieldset
  class="re-rating"
  data-size="lg"
  aria-label="Large rating"
  style="margin-block-start: var(--re-space-4)"
>
  <input class="re-sr-only" type="radio" name="r-lg" id="lg-3" value="3" checked />
  <label class="re-rating__star" for="lg-3" aria-label="3 stars">★</label>
  <input class="re-sr-only" type="radio" name="r-lg" id="lg-2" value="2" />
  <label class="re-rating__star" for="lg-2" aria-label="2 stars">★</label>
  <input class="re-sr-only" type="radio" name="r-lg" id="lg-1" value="1" />
  <label class="re-rating__star" for="lg-1" aria-label="1 star">★</label>
</fieldset>

For showing an average (no input), use .re-rating-display with role="img", an aria-label, and --re-rating-value — the filled overlay is clipped to value / max, so fractional values render a partial star.

★★★★★
Live example
<span
  class="re-rating-display"
  role="img"
  aria-label="Rated 3.5 out of 5"
  data-stars="★★★★★"
  style="--re-rating-value: 3.5"
  >★★★★★</span
>
  • Keyboard — it’s a native radio group: Tab moves into the group (landing on the checked star, or the first if none), and the arrow keys move the selection within it. Because the group is reversed, native arrow direction varies by browser; opt into enhanceRating and it normalizes to ArrowRight/ArrowUp = next-higher, ArrowLeft/ArrowDown = next-lower (by value, identically everywhere). From the cleared “No rating” state both arrows enter at the lowest star, and the value-0 and disabled radios are skipped during arrow navigation.
  • Focus:focus-visible on the focused (visually hidden) radio paints the --re-shadow-focus ring on its visible star.
  • Semantics — the <fieldset> carries an aria-label naming the rating, and each star <label> has an aria-label (“5 stars” … “1 star”) so the choices announce as a labelled radio group rather than bare glyphs. The read-only display is a single role="img" with an aria-label like “Rated 3.5 out of 5” — the stars are decorative text behind it.
  • Notes — the radios and the value-0 “No rating” label use .re-sr-only, so the clear option is reachable and announced without showing a sixth star; the direction: rtl on the fieldset is presentational only and the stars stay a low→high left-to-right scale regardless of page direction. Disabled via the native :disabled/aria-disabled="true". Under forced colors (HCM) the filled stars repaint with the system Highlight color and the focus ring becomes a real outline on the visible star, since the sr-only radio can’t show one. See the accessibility guide.