24 CSS Radio Buttons11 / 24

Pure CSSMIT licensed

Rating Star Radio Button Group

A five-star rating built the right way: five radio buttons, one per score, with cumulative fill — picking 4 lights stars 1-4 — plus hover preview and a staggered pop when the rating lands. No RTL row-reversal hacks, no JavaScript: the cumulative logic is a two-line :has() selector.

Published Updated

Live Demo
Try it

The code

<div class="crb-11-group">
<section class="crb-11a" aria-label="Star rating radio group">
  <fieldset class="crb-11a__group">
    <legend class="crb-11a__legend">Rate your experience</legend>
    <div class="crb-11a__stars">
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="1"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">1 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="2"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">2 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="3"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">3 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="4" checked><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">4 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="5"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">5 of 5 stars</span></label>
    </div>
    <p class="crb-11a__hint">Hover to preview · click or use arrow keys to rate</p>
  </fieldset>
</section>
</div>
.crb-11-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

.crb-11-group > section {
  flex: 1 1 340px;
  min-width: 300px;
}

.crb-11a,
.crb-11a *,
.crb-11a *::before,
.crb-11a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.crb-11a {
  --star: oklch(0.8 0.16 80);
  --ink: #292317;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #fdf9f0;
}

.crb-11a__group {
  border: none;
  display: grid;
  justify-items: center;
  gap: 16px;
}

.crb-11a__legend {
  font-size: 24px;
  font-weight: 750;
  color: var(--ink);
  letter-spacing: -.02em;
  text-align: center;
  margin-bottom: 4px;
}

.crb-11a__stars {
  display: flex;
  gap: 6px;
}

.crb-11a__star {
  position: relative;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 12px;
  cursor: pointer;
}

.crb-11a__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.crb-11a__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.crb-11a__glyph {
  width: 38px;
  height: 38px;
  fill: transparent;
  stroke: #d3c9ae;
  stroke-width: 1.6;
  transition: fill .25s,stroke .25s,transform .25s;
}

.crb-11a__star:has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked) .crb-11a__glyph {
  fill: var(--star);
  stroke: var(--star);
}

.crb-11a__stars:has(.crb-11a__input:checked) .crb-11a__star:nth-child(1):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__stars:has(.crb-11a__input:checked) .crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(1) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 0ms;
}

.crb-11a__star:nth-child(2):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(2) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 60ms;
}

.crb-11a__star:nth-child(3):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(3) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 120ms;
}

.crb-11a__star:nth-child(4):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(4) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 180ms;
}

.crb-11a__star:nth-child(5):has(.crb-11a__input:checked) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 240ms;
}

.crb-11a__stars:hover .crb-11a__star .crb-11a__glyph {
  fill: transparent;
  stroke: #d3c9ae;
}

.crb-11a__star:hover .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star:hover) .crb-11a__glyph {
  fill: color-mix(in oklab,var(--star) 55%,white)!important;
  stroke: var(--star)!important;
  transform: scale(1.08);
}

.crb-11a__star:has(.crb-11a__input:focus-visible) {
  outline: 3px solid var(--star);
  outline-offset: 2px;
}

.crb-11a__hint {
  font-size: 13px;
  color: #a49879;
}

@keyframes crb-11a-pop {
  0% {
    transform: scale(.6);
  }

  60% {
    transform: scale(1.25);
  }

  100% {
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .crb-11a__glyph {
    transition: fill .2s,stroke .2s;
    animation: none!important;
  }

  .crb-11a__star:hover .crb-11a__glyph {
    transform: none;
  }
}
/* No JavaScript — label:has(~ label input:checked) fills every star before the checked one (logical DOM order, no RTL hack); the hover trail uses the same pattern with :hover. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Radio Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Rating Star Radio Button Group
Source: https://codefronts.com/components/css-radio-buttons/star-burst/

A five-star rating built the right way: five radio buttons, one per score, with cumulative fill — picking 4 lights stars 1-4 — plus hover preview and a staggered pop when the rating lands. No RTL row-reversal hacks, no JavaScript: the cumulative logic is a two-line :has() selector.
## HTML
```html
<div class="crb-11-group">
<section class="crb-11a" aria-label="Star rating radio group">
  <fieldset class="crb-11a__group">
    <legend class="crb-11a__legend">Rate your experience</legend>
    <div class="crb-11a__stars">
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="1"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">1 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="2"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">2 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="3"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">3 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="4" checked><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">4 of 5 stars</span></label>
      <label class="crb-11a__star"><input class="crb-11a__input" type="radio" name="crb-11a" value="5"><svg class="crb-11a__glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2.6l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.4l-5.8 3.1 1.1-6.5L2.6 9.4l6.5-.9z"/></svg><span class="crb-11a__sr">5 of 5 stars</span></label>
    </div>
    <p class="crb-11a__hint">Hover to preview · click or use arrow keys to rate</p>
  </fieldset>
</section>
</div>
```
## CSS
```css
.crb-11-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
}

.crb-11-group > section {
  flex: 1 1 340px;
  min-width: 300px;
}

.crb-11a,
.crb-11a *,
.crb-11a *::before,
.crb-11a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.crb-11a {
  --star: oklch(0.8 0.16 80);
  --ink: #292317;
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: #fdf9f0;
}

.crb-11a__group {
  border: none;
  display: grid;
  justify-items: center;
  gap: 16px;
}

.crb-11a__legend {
  font-size: 24px;
  font-weight: 750;
  color: var(--ink);
  letter-spacing: -.02em;
  text-align: center;
  margin-bottom: 4px;
}

.crb-11a__stars {
  display: flex;
  gap: 6px;
}

.crb-11a__star {
  position: relative;
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 12px;
  cursor: pointer;
}

.crb-11a__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.crb-11a__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.crb-11a__glyph {
  width: 38px;
  height: 38px;
  fill: transparent;
  stroke: #d3c9ae;
  stroke-width: 1.6;
  transition: fill .25s,stroke .25s,transform .25s;
}

.crb-11a__star:has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked) .crb-11a__glyph {
  fill: var(--star);
  stroke: var(--star);
}

.crb-11a__stars:has(.crb-11a__input:checked) .crb-11a__star:nth-child(1):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__stars:has(.crb-11a__input:checked) .crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(1) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 0ms;
}

.crb-11a__star:nth-child(2):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(2) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 60ms;
}

.crb-11a__star:nth-child(3):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(3) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 120ms;
}

.crb-11a__star:nth-child(4):has(.crb-11a__input:checked) .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star .crb-11a__input:checked):nth-child(4) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 180ms;
}

.crb-11a__star:nth-child(5):has(.crb-11a__input:checked) .crb-11a__glyph {
  animation: crb-11a-pop .4s cubic-bezier(.34,1.56,.64,1) 240ms;
}

.crb-11a__stars:hover .crb-11a__star .crb-11a__glyph {
  fill: transparent;
  stroke: #d3c9ae;
}

.crb-11a__star:hover .crb-11a__glyph,
.crb-11a__star:has(~ .crb-11a__star:hover) .crb-11a__glyph {
  fill: color-mix(in oklab,var(--star) 55%,white)!important;
  stroke: var(--star)!important;
  transform: scale(1.08);
}

.crb-11a__star:has(.crb-11a__input:focus-visible) {
  outline: 3px solid var(--star);
  outline-offset: 2px;
}

.crb-11a__hint {
  font-size: 13px;
  color: #a49879;
}

@keyframes crb-11a-pop {
  0% {
    transform: scale(.6);
  }

  60% {
    transform: scale(1.25);
  }

  100% {
    transform: scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .crb-11a__glyph {
    transition: fill .2s,stroke .2s;
    animation: none!important;
  }

  .crb-11a__star:hover .crb-11a__glyph {
    transform: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — label:has(~ label input:checked) fills every star before the checked one (logical DOM order, no RTL hack); the hover trail uses the same pattern with :hover. */
```

How this works

The old CSS star-rating trick reversed the DOM and used sibling selectors backwards. :has() kills the hack: label:has(~ label input:checked) matches every star before the checked one, so DOM order stays logical (1→5) for screen readers and arrow keys. The same pattern with :hover ~ gives the preview trail.

Each star is an inline SVG whose fill and stroke transition together; on check, filled stars replay a scale-pop keyframe with a per-star animation-delay so the fill sweeps left to right. Labels carry <span class="sr"> text ("3 of 5 stars") so the group reads as a rating, not five unlabeled radios.

Make it yours

  • Gold lives in --star; try coral or brand colours.
  • Sweep speed = the 60ms stagger step on each nth-child delay.
  • Swap the SVG path for hearts or thumbs — logic is unchanged.
  • Show the current score by styling the caption with :has() per value.

Gotchas — read before shipping

  • Keep DOM order 1→5 — the :has(~) trick exists precisely so you don't reverse it.
  • Every label needs visually-hidden text; a star glyph alone is not a label.
  • Hover preview must not persist after pointerleave — style only :hover chains, never toggle classes.

Browser support

ChromeSafariFirefoxEdge
111+16.2+121+111+

Floor set by :has(), oklch(), color-mix() as this demo is written. The core technique itself goes back further — Chrome 105+.

The cumulative fill is 100% dependent on :has(); on older browsers stars select correctly but only the picked star fills.

Search CodeFronts

Loading…