Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Three-layer star rating widget: CSS-only selection and hover, JS-enhanced clear/events, form-associated web component.
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.
The rating widget follows the progressive enhancement model:
:has() selectors handle visual selection and hover. Works without JS.rating-init.js adds clear/unrate (click selected star again), rating:change events, and screen reader announcements.<star-rating> generates the fieldset internally. Form-associated via ElementInternals — no hidden inputs needed.Write a <fieldset data-rating> with radio inputs. The CSS handles selection highlighting and hover preview via :has() selectors. No JavaScript required.
<fieldset data-rating> <legend>Rate this product</legend> <label><input type="radio" name="rating" value="1" aria-label="1 star">★</label> <label><input type="radio" name="rating" value="2" aria-label="2 stars">★</label> <label><input type="radio" name="rating" value="3" aria-label="3 stars">★</label> <label><input type="radio" name="rating" value="4" aria-label="4 stars">★</label> <label><input type="radio" name="rating" value="5" aria-label="5 stars">★</label></fieldset>
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. For the web component, use the allow-half attribute instead.
<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">★</label> <label data-half="right"><input type="radio" name="r" value="1" aria-label="1 star">★</label> <!-- ...repeat for each star... --></fieldset>
Replace the star character with any <icon-wc> icon. The :has() selectors work the same way — they target the label, not the star character.
<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>
When rating-init.js is loaded (included in the default bundle), data-rating fieldsets gain:
rating:change event: Fires on the fieldset with { detail: { value } }const rating = document.querySelector('[data-rating]'); rating.addEventListener('rating:change', (e) => { console.log('New rating:', e.detail.value);});
The <star-rating> web component generates the fieldset internally and adds form participation via ElementInternals.
<star-rating name="product-rating" label="Rate this product"></star-rating>
<star-rating name="rating" value="3.5" allow-half label="Your rating"></star-rating>
<star-rating value="4.2" readonly label="Average rating"></star-rating>
<star-rating name="rating" icon="heart" max="3" label="How much?"></star-rating>
| 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) |
allow-half |
— | Enable half-star increments |
readonly |
— | Display-only mode, no interaction |
icon |
— | Lucide icon name (uses <icon-wc>) |
required |
— | Makes rating required for form validation |
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> <star-rating name="rating" required label="Your rating"></star-rating> <button type="submit">Submit</button></form>
| 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.
| Event | Detail | Description |
|---|---|---|
rating:change |
{ value: number } |
Fired on the fieldset when the rating value changes (including clear to 0). |
<fieldset> + <legend> groups the radios with an accessible labelaria-label announcing the star countrole="img" with a descriptive aria-labelOverride 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-error);} /* Custom size */[data-rating] > label { font-size: 2rem;}
Each layer adds capability without breaking the previous one:
ElementInternals<icon-wc> — Custom icons for rating labels<form-field> — Form field wrapper with validation<fieldset> — Native grouping element used internally