rating-wc

Three-layer star rating widget: CSS-only selection and hover, JS-enhanced clear/events, form-associated web component.

Overview

A star rating widget built the Vanilla Breeze way — CSS-first with native HTML, enhanced with JS, wrapped in a web component for convenience. Supports half-stars, custom icons via <icon-wc>, read-only display, and native form participation.

Three Layers

The rating widget follows the progressive enhancement model:

  1. CSS-only — Author writes a fieldset with radio inputs. :has() selectors handle visual selection and hover. Works without JS.
  2. JS enhancementrating-init.js adds clear/unrate (click selected star again), rating-change events, and screen reader announcements.
  3. Web component<rating-wc> generates the fieldset internally. Form-associated via ElementInternals — no hidden inputs needed.

CSS-Only Usage

Write a <fieldset data-rating> with radio inputs. The CSS handles selection highlighting and hover preview via :has() selectors. No JavaScript required.

Rate this product
<fieldset data-rating> <legend>Rate this product</legend> <label><input type="radio" name="rating" value="1" aria-label="1 star">&#9733;</label> <label><input type="radio" name="rating" value="2" aria-label="2 stars">&#9733;</label> <label><input type="radio" name="rating" value="3" aria-label="3 stars">&#9733;</label> <label><input type="radio" name="rating" value="4" aria-label="4 stars">&#9733;</label> <label><input type="radio" name="rating" value="5" aria-label="5 stars">&#9733;</label> </fieldset>

Half Stars

Add data-rating-half to the fieldset and use data-half="left" / data-half="right" on labels. Each star gets two radio inputs: one for the half value, one for the full value.

Half-star rating
<fieldset data-rating data-rating-half> <legend>Rate this</legend> <label data-half="left"><input type="radio" name="r" value="0.5" aria-label="Half star">&#9733;</label> <label data-half="right"><input type="radio" name="r" value="1" aria-label="1 star">&#9733;</label> <!-- ...repeat for each star... --> </fieldset>

Custom Icons

Replace the star character with any <icon-wc> icon. The :has() selectors work the same way — they target the label, not the star character.

How much do you love this?
<fieldset data-rating> <legend>How much do you love this?</legend> <label><input type="radio" name="love" value="1" aria-label="1 heart"><icon-wc name="heart"></icon-wc></label> <label><input type="radio" name="love" value="2" aria-label="2 hearts"><icon-wc name="heart"></icon-wc></label> <!-- ...etc... --> </fieldset>

JS Enhancement

When rating-init.js is loaded (included in the default bundle), data-rating fieldsets gain:

  • Clear/unrate: Click the already-selected star to deselect (radio unchecked, value resets to 0)
  • rating-change event: Fires on the fieldset with { detail: { value } }
  • Screen reader announcements: Live region announces the current value
const rating = document.querySelector('[data-rating]'); rating.addEventListener('rating-change', (e) => { console.log('New rating:', e.detail.value); });

Web Component

The <rating-wc> web component generates the fieldset internally and adds form participation via ElementInternals.

Simple

<rating-wc name="product-rating" label="Rate this product"></rating-wc>

Half-star with preset value

<rating-wc name="rating" value="3.5" data-half label="Your rating"></rating-wc>

Read-only display

<rating-wc value="4.2" data-readonly label="Average rating"></rating-wc>

Custom icons

<rating-wc name="rating" data-icon="heart" max="3" label="How much?"></rating-wc>

Attributes

Attribute Default Description
name Form field name (omit for read-only)
value 0 Current rating value
max 5 Number of stars
label "Rating" Legend text (visually hidden)
data-half Enable half-star increments
data-readonly Display-only mode, no interaction
data-icon Lucide icon name (uses <icon-wc>)
required Makes rating required for form validation

Form Participation

The web component uses ElementInternals to participate in native forms — no hidden inputs. It supports FormData, the required attribute, Constraint Validation API, form.reset(), and :invalid/:valid pseudo-classes.

<form> <rating-wc name="rating" required label="Your rating"></rating-wc> <button type="submit">Submit</button> </form>

Keyboard Navigation

Key Action
ArrowRight / ArrowDown Select next star
ArrowLeft / ArrowUp Select previous star
Tab Move focus into/out of the rating group

Keyboard navigation is provided natively by the radio group — no custom JavaScript needed.

Events

Event Detail Description
rating-change { value: number } Fired on the fieldset when the rating value changes (including clear to 0).

Accessibility

  • <fieldset> + <legend> groups the radios with an accessible label
  • Each radio has aria-label announcing the star count
  • Native radio keyboard navigation (arrow keys) requires no custom handling
  • Focus-visible outline appears on the active star label
  • JS enhancement adds screen reader announcements via live region
  • Read-only mode uses role="img" with a descriptive aria-label

Styling

Override the rating color and size with CSS custom properties or direct selectors:

/* Custom color */ [data-rating] > label:has(input:checked), [data-rating] > label:has(~ label > input:checked) { color: var(--color-danger); } /* Custom size */ [data-rating] > label { font-size: 2rem; }

Progressive Enhancement

Each layer adds capability without breaking the previous one:

  • No JS: CSS-only radios work for selection, hover preview, form submission, and keyboard navigation
  • With JS: Clear/unrate, events, and announcements enhance the experience
  • Web component: Convenience wrapper that generates the markup and adds form participation via ElementInternals

Related Elements