Accessibility pattern · Form inputs

Star rating

Choosing a rating is choosing one of five, so the stars are radio buttons in a fieldset: the arrow keys, the single tab stop and the checked state come from the browser. Showing an average is not a control at all; it is text, "4.3 out of 5", with the stars drawn beside it as decoration.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Brass table lamp

Delivered on 2 October

Rate this product

Not rated yet

Customer reviews

4.3 out of 5

128 reviews

  • 5 stars74 reviews
  • 4 stars32 reviews
  • 3 stars11 reviews
  • 2 stars6 reviews
  • 1 star5 reviews

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves into the stars, onto the chosen one (or the first if none is chosen), then on to Clear rating.
Arrow Right or Arrow DownChooses one more star, wrapping from 5 to 1.
Arrow Left or Arrow UpChooses one star fewer, wrapping from 1 to 5.
SpaceChooses the focused star when none is chosen yet.
Enter or SpaceOn Clear rating, removes the rating and puts focus back on the first star.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the stars before ratingRate this product, group. 1 star, radio button, not checked, 1 of 5
Arrow Right chooses 4 stars4 stars, radio button, checked, 4 of 5
The choice settles for a momentRating saved
Reading the averageCustomer reviews, heading level 3. 4.3 out of 5. 128 reviews
Reading the breakdownReviews by rating, list, 5 items. 5 stars 74 reviews

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-rating" data-ap-rating>
  <form class="ap-rating__card" data-ap-rate>
    <div class="ap-rating__product">
      <span class="ap-rating__thumb" aria-hidden="true">
        <svg viewBox="0 0 24 24" focusable="false"><path d="M8 3h8l3 8H5Z"/><path d="M12 11v7"/><path d="M8 21h8"/><path d="M12 18v3"/></svg>
      </span>
      <div>
        <p class="ap-rating__item">Brass table lamp</p>
        <p class="ap-rating__meta">Delivered on 2 October</p>
      </div>
    </div>
    <fieldset class="ap-rating__set">
      <legend class="ap-rating__legend">Rate this product</legend>
      <div class="ap-rating__stars">
        <label class="ap-rating__star">
          <input class="ap-rating__radio" type="radio" name="rating-score" value="1" data-word="Poor" />
          <svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg>
          <span class="ap-rating__sr">1 star</span>
        </label>
        <label class="ap-rating__star">
          <input class="ap-rating__radio" type="radio" name="rating-score" value="2" data-word="Fair" />
          <svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg>
          <span class="ap-rating__sr">2 stars</span>
        </label>
        <label class="ap-rating__star">
          <input class="ap-rating__radio" type="radio" name="rating-score" value="3" data-word="Good" />
          <svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg>
          <span class="ap-rating__sr">3 stars</span>
        </label>
        <label class="ap-rating__star">
          <input class="ap-rating__radio" type="radio" name="rating-score" value="4" data-word="Very good" />
          <svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg>
          <span class="ap-rating__sr">4 stars</span>
        </label>
        <label class="ap-rating__star">
          <input class="ap-rating__radio" type="radio" name="rating-score" value="5" data-word="Excellent" />
          <svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg>
          <span class="ap-rating__sr">5 stars</span>
        </label>
        <span class="ap-rating__word" aria-hidden="true" data-ap-word></span>
      </div>
    </fieldset>
    <div class="ap-rating__foot">
      <p class="ap-rating__chosen" data-ap-chosen>Not rated yet</p>
      <button type="button" class="ap-btn ap-btn--ghost ap-rating__clear" data-ap-unrate hidden>Clear rating</button>
    </div>
    <p class="ap-rating__saved" role="status" data-ap-say></p>
  </form>

  <section class="ap-rating__card ap-rating__summary" aria-labelledby="rating-avg">
    <h3 class="ap-rating__sum" id="rating-avg">Customer reviews</h3>
    <div class="ap-rating__avg">
      <span class="ap-rating__meter" aria-hidden="true" data-ap-value="4.3" data-ap-of="5">
        <span class="ap-rating__row"><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg></span>
        <span class="ap-rating__row ap-rating__row--fill"><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg><svg class="ap-rating__shape" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.8l2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9Z"/></svg></span>
      </span>
      <p class="ap-rating__score">4.3 out of 5</p>
      <p class="ap-rating__count">128 reviews</p>
    </div>
    <ul class="ap-rating__dist" aria-label="Reviews by rating">
      <li class="ap-rating__dist-row"><span class="ap-rating__dist-k">5 stars</span><span class="ap-rating__bar" aria-hidden="true"><span data-ap-share="74"></span></span><span class="ap-rating__dist-n"><span translate="no">74</span><span class="ap-rating__sr"> reviews</span></span></li>
      <li class="ap-rating__dist-row"><span class="ap-rating__dist-k">4 stars</span><span class="ap-rating__bar" aria-hidden="true"><span data-ap-share="32"></span></span><span class="ap-rating__dist-n"><span translate="no">32</span><span class="ap-rating__sr"> reviews</span></span></li>
      <li class="ap-rating__dist-row"><span class="ap-rating__dist-k">3 stars</span><span class="ap-rating__bar" aria-hidden="true"><span data-ap-share="11"></span></span><span class="ap-rating__dist-n"><span translate="no">11</span><span class="ap-rating__sr"> reviews</span></span></li>
      <li class="ap-rating__dist-row"><span class="ap-rating__dist-k">2 stars</span><span class="ap-rating__bar" aria-hidden="true"><span data-ap-share="6"></span></span><span class="ap-rating__dist-n"><span translate="no">6</span><span class="ap-rating__sr"> reviews</span></span></li>
      <li class="ap-rating__dist-row"><span class="ap-rating__dist-k">1 star</span><span class="ap-rating__bar" aria-hidden="true"><span data-ap-share="5"></span></span><span class="ap-rating__dist-n"><span translate="no">5</span><span class="ap-rating__sr"> reviews</span></span></li>
    </ul>
  </section>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.1.1 Non-text Content Level A

    Each star has a text name, 1 star to 5 stars, and the average is written as text, so no meaning is carried by the drawings alone.

  • 1.3.1 Info and Relationships Level A

    The stars are radio buttons in a fieldset whose legend asks the question; the average is a heading, text and a list.

  • 1.4.1 Use of Color Level A

    Chosen stars are filled, not just recolored, and the choice is also written out below them.

  • 1.4.11 Non-text Contrast Level AA

    Empty stars are outlined at 3:1 or more against the card, so all five are visible before any is chosen.

  • 2.1.1 Keyboard Level A

    The arrow keys choose a rating, as in any radio group; Clear rating is a button.

  • 2.4.7 Focus Visible Level AA

    The focused star shows a ring, drawn around its label because the radio itself is transparent.

  • 4.1.3 Status Messages Level AA

    Saving is confirmed in a status message once the choice settles, not on every arrow press.

Usage

When to use it

Use it

  • Asking for a quick opinion on a scale, like rating a purchase, a recipe or a support answer.
  • Showing an average rating next to a product, a place or a course.

Use something else

  • Questions with named answers, like Satisfied or Not satisfied: use a plain radio group with those words.
  • Fine-grained scores, like 1 to 10: use a number input or a slider.

Common failures

How it usually goes wrong

  • Stars made of clickable spans

    Five icons with click handlers cannot be reached by keyboard or told apart by a screen reader. Native radio buttons give both.

  • Hover that changes the rating

    Committing on hover lets a pointer passing over the stars wipe out a choice. Hovering here only previews; a click or a key chooses.

  • An average drawn only as stars

    Four and a third yellow stars mean nothing to a screen reader or in high contrast mode. The number is written out, and the drawing is hidden.

  • The average as a disabled control

    A read-only slider or a disabled radio group for an average is noise: people tab to it, or skip it as unavailable. Text is the right element.

  • No way back to no rating

    A radio group cannot be unchecked by itself. Clear rating gives people a way out after an accidental click.

Notes

Building it

  • The radio input covers its star, transparent, so the whole star is the target and keeps its native focus and keys; the ring is drawn with :has(:focus-visible).
  • The fill comes from :has(): a star is filled when its own radio, or any later one, is checked. The hover preview uses the same trick with :hover.
  • The partial star in the average is a filled row of stars clipped at 4.3 of them; the script measures that from data-ap-value, and the text beside it says the same.
  • The rating saves after the choice has been still for a moment, so arrowing from 1 to 5 sends one request, not four.

Sources: WAI-ARIA Authoring Practices: Radio Group · HTML: the fieldset element

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction