Skip to content

Components

Star Ratings Are Radio Groups — the Accessible Rating Input Nobody Builds

Five Hidden Radios, Painted Stars, Zero ARIA Gymnastics
Written by Aiden Wood Founder and Lead Design Engineer
⭐ 🔘 🖱️

Search any codebase for a star rating and you'll find five <div>s with onClick handlers. Mouse-only, invisible to screen readers, no form value, no keyboard. For a control whose entire job is collecting an opinion, it collects it from the narrowest possible audience.

Here's the reframe that fixes everything at once: a star rating is a radio group. Five options, exactly one selectable — the browser has shipped this control since 1995. Velocity X's rating-input is built on that bones.

The Money Pattern

Real radio inputs, visually hidden; the stars are the labels:

<fieldset>
  <legend class="sr-only">Rate your experience</legend>
  <div class="flex" onMouseLeave={() => setPreview(0)}>
    {[1, 2, 3, 4, 5].map((n) => (
      <label onMouseEnter={() => setPreview(n)}>
        <input type="radio" name="rating" value={n}
               checked={value === n}
               onChange={() => setValue(n)}
               class="sr-only" />
        <Star filled={n <= (preview || value)} />
        <span class="sr-only">{n} star{n > 1 && 's'}</span>
      </label>
    ))}
  </div>
</fieldset>

Everything hard arrives free: arrow keys move between stars (radio group semantics), Tab treats the group as one stop, screen readers announce "3 stars, radio button, 3 of 5", and the value submits with the form like any input. The only custom state is preview for the hover fill — a purely visual layer over a working control.

The Catch

Half-star ratings break the radio model's neat mapping — ten radios painted as five stars works, but the hover math gets fiddly and the keyboard step becomes 0.5. Ask whether half stars change any decision your product makes; display can be fractional (averages) while input stays whole.

The other trap is hover-commit: setting the actual value on mouseenter instead of click. A rating isn't chosen until it's clicked — preview and value are different states, and collapsing them turns a stray mouse movement into a 1-star review.

The Verdict

Reach for the platform control wearing custom paint, not a div sculpture re-implementing it badly. Radio group + sr-only + preview state is the whole component. It's in Velocity X's Forms category with the other inputs that look simple and aren't — source you own, restyled by your Tailwind config.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.