← Back to gallery
CSS

Rating Star Fill Hover Preview

Three star-rating treatments tuned per product context — a Classic Five (canonical product rating), a Ten-Tier (fine-grain reviewer scale, faster transition), and a Three-Tier (emoji-style quick survey, deliberate slower transition). All three use real <input type="radio"> + <label> so keyboard, focus-visible, and screen-reader semantics come from the native control; pointer hover fills a preview range via the sibling combinator while the checked state remains the source of truth.

ratingstar-fillhover-previewradio-inputsibling-combinatorkeyboard-a11yprefers-reduced-motion

Interaction · radio-input · hover preview

Rating Star Fill Hover Preview

Three star-rating treatments tuned per product context — a Classic Five (canonical product rating), a Ten-Tier (fine-grain reviewer scale), and a Three-Tier (emoji-style quick survey). All three are real <input type="radio"> elements so keyboard / focus-visible / screen-reader semantics come from the native control; pointer hover fills a preview range via the sibling combinator while the checked state remains the source of truth. Stage cards animate a sequential star fill via CSS @keyframes synced with the gallery thumbnail; inspector preview is fully interactive.

Product rating · canonical 5-star

Classic Five

Five radio inputs render as stars with a hover preview fill that sweeps in on pointer-over and a checked state that persists the user choice. The canonical product-rating UI; works for review submissions, library ratings, and any 1–5 evaluation.

  • 5 star
  • product rating
  • hover sweep

Reviewer · fine-grain 10-star

Ten-Tier

Ten stars give critics and reviewers half-increments visually — suited to editorial UIs where five would feel too coarse. Tighter spacing keeps the row from breaking; a faster transition handles sweep across many targets without lag.

  • 10 star
  • critic
  • tight spacing

Quick survey · poor / okay / great

Three-Tier

Three stars serve as poor / okay / great. The slower transition reads more deliberate so the user feels the selection click into place. Use for in-app quick surveys where nuance is unnecessary; keeping the states coarse avoids implicit promise of a 1–5 scale.

  • 3 tier
  • quick survey
  • deliberate

Rating star inspector

Classic Five

hover stars
  • 5 star
  • product rating
  • hover sweep

Five radio inputs render as stars with a hover preview fill that sweeps in on pointer-over and a checked state that persists the user choice. The canonical product-rating UI; works for review submissions, library ratings, and any 1–5 evaluation.

Helped you ship something? 🐟 Send my cat a churu

/* Five-star product rating with sibling-combinator hover preview. */
.star-rating {
  display: inline-flex;
  /* row-reverse so the sibling combinator (~) targets stars to the
     LEFT of the hover target — left of in DOM = right of visually. */
  flex-direction: row-reverse;
  gap: 4px;
}

.star-rating input { position: absolute; opacity: 0; pointer-events: none; }

.star-rating label {
  cursor: pointer;
  color: rgba(148, 163, 184, 0.28);
  transition: color 0.12s ease;
}

/* Checked state: persists the selection. */
.star-rating input:checked ~ label {
  color: #fde047;
}

/* While the row is hovered, reset every label so the hover trail
   below can re-fill from the pointer outward. */
.star-rating:hover input ~ label {
  color: rgba(148, 163, 184, 0.28);
}

/* Hover preview: hovered star + every later sibling fill. The
   .star-rating:hover qualifier lifts specificity so the hover trail
   wins against the reset rule above. */
.star-rating:hover label:hover,
.star-rating:hover label:hover ~ label {
  color: #fde047;
}

@media (prefers-reduced-motion: reduce) {
  .star-rating label { transition: none; }
}

How to make this

A rating star hover preview uses real radio inputs, renders labels in reverse order, and lets the sibling combinator fill the preview trail while checked state remains canonical.

html
1<fieldset class="star-rating-demo">  <legend>Rate this pattern</legend>3  <input id="rate-5" name="rating" type="radio" value="5">  <label for="rate-5" aria-label="5 stars">&#9733;</label>  <input id="rate-4" name="rating" type="radio" value="4" checked>  <label for="rate-4" aria-label="4 stars">&#9733;</label>  <input id="rate-3" name="rating" type="radio" value="3">  <label for="rate-3" aria-label="3 stars">&#9733;</label>  <input id="rate-2" name="rating" type="radio" value="2">  <label for="rate-2" aria-label="2 stars">&#9733;</label>  <input id="rate-1" name="rating" type="radio" value="1">  <label for="rate-1" aria-label="1 star">&#9733;</label></fieldset> <style>.star-rating-demo {17  display: inline-flex;  flex-direction: row-reverse;  gap: .25rem;  margin: 0;  padding: .4rem;  border: 0;}.star-rating-demo legend {  position: absolute;  inline-size: 1px;  block-size: 1px;  overflow: hidden;  clip: rect(0 0 0 0);}.star-rating-demo input {32  position: absolute;  opacity: 0;}.star-rating-demo label {  color: rgba(148, 163, 184, .32);  cursor: pointer;  font-size: 2rem;  line-height: 1;  transition: color .12s ease, transform .12s ease;}42.star-rating-demo input:checked ~ label {  color: #fde047;}45.star-rating-demo:hover input ~ label {  color: rgba(148, 163, 184, .32);}.star-rating-demo:hover label:hover,.star-rating-demo:hover label:hover ~ label {  color: #fde047;  transform: scale(1.08);52}.star-rating-demo input:focus-visible + label {  outline: 2px solid #7dd3fc;  outline-offset: 4px;  border-radius: 6px;}@media (prefers-reduced-motion: reduce) {  .star-rating-demo label { transition: none; transform: none; }  .star-rating-demo:hover label:hover,  .star-rating-demo:hover label:hover ~ label { transform: none; }}</style>

Annotated snippet

  1. Line 1Use a fieldset because this is a single-choice group. The legend can be visually hidden, but the group still needs a name.
    PitfallShould star ratings use buttons or radio inputs?

    Use radios when the user chooses one value from a fixed scale. Radios provide arrow-key navigation, checked state, form integration, and useful accessibility mappings without custom JavaScript.

  2. Line 3Each star is a real radio input. Arrow keys, checked state, form submission, and screen reader behavior come from the native control.
    PitfallShould star ratings use buttons or radio inputs?

    Use radios when the user chooses one value from a fixed scale. Radios provide arrow-key navigation, checked state, form integration, and useful accessibility mappings without custom JavaScript.

  3. Line 17row-reverse is the trick that makes the general sibling combinator fill stars to the visual left of the matched input.

    Imagine the user hovers the 3-star (▲ marker). The same CSS rule .on, .on ~ i { color: gold } fires on both rows. In the normal row the trail paints the THREE HIGHER stars on the right — wrong direction for a rating. row-reverse flips the visual layout, so the same DOM siblings now render on the left and the three LOWER stars light up correctly.

    PitfallWhy render the stars in reverse order?

    CSS can only select following siblings with ~. Reversing the visual row lets the same selector paint the hovered or checked star plus every lower-value star to its visual left.

  4. Line 32The radios are visually hidden but still present. Avoid display: none because that removes them from keyboard and accessibility behavior.
  5. Line 42The checked selector paints the persisted value. This is the source of truth when the pointer is not previewing another value.
    PitfallHow do I keep hover preview from changing the saved rating?

    Treat hover as temporary CSS only. The checked radio remains the canonical value, and only a click, tap, keyboard Space, or form control change should update it.

  6. Line 45On row hover, reset the checked trail first. The next rule can then draw a temporary preview from the hovered label without changing the radio value.

    Saved: 4★ (orange = persisted). Pointer is now over the 2★ to preview lowering the rating. Without the row-hover reset, the checked orange trail (4 stars) stays visible AND the hover yellow trail (2 stars) paints on top — leftmost 4 stars end up in mixed orange/yellow, you cannot tell what the new rating preview is. With the reset, hover wipes the orange first, so only the clean 2-yellow preview shows.

    PitfallHow do I keep hover preview from changing the saved rating?

    Treat hover as temporary CSS only. The checked radio remains the canonical value, and only a click, tap, keyboard Space, or form control change should update it.

  7. Line 52Focus-visible is attached to the label adjacent to the focused radio, so keyboard users can see which star currently has focus.
    PitfallWhat should reduced motion do for rating stars?

    Remove scale and transition timing but keep checked, hover, and focus-visible states. Reduced motion should not remove the preview or the selected value.

Other pitfalls

Are star hover transitions expensive?
Color and small transform changes on a handful of labels are cheap. Avoid animating SVG filters, text shadows, or layout properties across large review tables with hundreds of rating rows.
Advanced

Cascade a pulse across filled stars and land it on the boundary

Same five-star row, four filled yellow + one gray empty (a 4-out-of-5 rating). Same 2.4s cycle. BEFORE leaves the four filled stars in flat static yellow — the rating registers, but the eye has to count to find the boundary. AFTER cascades a 2.4s pulse across the filled stars staggered 100ms by visual position (left to right) and lands a stronger glow on the boundary star (the 4th, adjacent to the gray empty star). The wave directs attention to the rating value, not just to "you rated it".

View explanation and full code36 lines

The base recipe lights all four filled stars with the same flat yellow — confirmed but visually quiet. Production rating UIs (Apple App Store, Goodreads) cascade a gentle 2.4s pulse across the filled stars in visual order (left to right, 100ms stagger) and land a stronger glow on the BOUNDARY star — the one adjacent to the empty star that represents the rating value. The wave directs the eye toward the rating answer instead of leaving it to count. animation-delay drives the stagger; input:checked + label targets the boundary for the stronger glow.

Append these rules inside the <style> block from the base snippet above.

css
/* Advanced: cascade pulse + boundary glow — extends the base recipe. */.star-rating-demo input:checked ~ label {  animation: star-rating-recipe-pulse 2.4s ease-in-out infinite;}/* Stagger by visual position. In row-reverse layout, label-rate-1   (nth-of-type 5) is visually leftmost; label-rate-4 (nth-of-type 2)   is the boundary star next to the empty rate-5. The cascade flows   from leftmost filled toward the boundary. */.star-rating-demo label:nth-of-type(5) { animation-delay: 0ms;    }.star-rating-demo label:nth-of-type(4) { animation-delay: 100ms;  }.star-rating-demo label:nth-of-type(3) { animation-delay: 200ms;  }.star-rating-demo label:nth-of-type(2) { animation-delay: 300ms;  } /* Boundary star (adjacent to the checked input) gets the stronger glow. */.star-rating-demo input:checked + label {  animation: star-rating-recipe-boundary 2.4s ease-in-out infinite;  animation-delay: 300ms;}.star-rating-demo:hover input:checked ~ label,.star-rating-demo:hover input:checked + label {  animation: none;} @keyframes star-rating-recipe-pulse {  0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 3px rgba(253, 224, 71, .35)); }  50%      { transform: scale(1.04); filter: drop-shadow(0 0 8px rgba(253, 224, 71, .55)); }}@keyframes star-rating-recipe-boundary {  0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 5px rgba(253, 224, 71, .5)); }  50%      { transform: scale(1.12); filter: drop-shadow(0 0 16px rgba(253, 224, 71, .85)); }} @media (prefers-reduced-motion: reduce) {  .star-rating-demo input:checked ~ label,  .star-rating-demo input:checked + label { animation: none; }}

Notes

Overview

Star rating control built on real <input type="radio"> in reverse order so the sibling combinator (.star-rating input:checked ~ label) can paint fills onto stars to the right of (visually before) the checked input. Hover previews work via the same combinator with :hover in place of :checked.

When to use it

Reach for star ratings on review surfaces, feedback forms, any place a 1-5 (or 1-10) selection needs to feel tactile. Skip stars for content where rating granularity matters beyond the visible step count (use a slider). Skip them when the rating is one-shot and read-only — static SVG icons read clearer.

How it works

Render five radio inputs in DOM order 5, 4, 3, 2, 1 (reversed) with flex-direction: row-reverse so they display left-to-right but the markup is right-to-left. This gives the sibling combinator the geometry it needs: input:checked ~ label selects all labels after the checked input in DOM order, which visually means all stars to the left of the selected star — exactly what should be filled. Hover preview uses the same selector with :hover: .stars:hover input ~ label { fill: none } clears all fills, then label:hover, label:hover ~ label { fill: gold } paints the hovered star and everything visually before it.

Production gotchas

Reverse-order DOM hurts keyboard navigation because tab order follows DOM order — arrow keys jump in reverse-visual order. Wrap the radio group with a directional aria-label describing the scale (“Rate from 1 to 5 stars”) and accept that this trade-off favors visual elegance over keyboard ergonomics; for accessibility-critical surfaces use real<button>s with JS state instead. The sibling combinator only walks forward, so it cannot fill stars on the wrong visual side — if the reversed layout is breaking flex-direction, double-check the parent does not override row-reverse.

Accessibility

The native radio inputs carry the accessible name from their <label> children — screen readers announce “5 stars” etc. correctly. Keep the aria-label on the wrapping <fieldset> describing what is being rated. Under prefers-reduced-motion: reduce drop the fill transition so stars snap to filled state without the sliding paint. Verify keyboard arrow key behavior reads in visual left-to-right order despite the reversed DOM.

References

Implementation depth

The hover preview should not replace the committed rating. Radio inputs keep the saved value clear, while sibling selectors can preview future fills as the pointer or keyboard moves through options.

Keyboard behavior is the test that matters. Arrow keys should move through values predictably, the selected value should be announced, and reduced motion should keep the star fill state immediate.