Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><symbol id="si-star" viewBox="0 0 20 20"><path d="M9.05 2.93c.3-.92 1.6-.92 1.9 0l1.07 3.29a1 1 0 0 0 .95.69h3.46c.97 0 1.37 1.24.59 1.81l-2.8 2.03a1 1 0 0 0-.36 1.12l1.07 3.3c.3.91-.76 1.68-1.54 1.11l-2.8-2.03a1 1 0 0 0-1.18 0l-2.8 2.03c-.78.57-1.84-.2-1.54-1.11l1.07-3.3a1 1 0 0 0-.36-1.12L2.98 8.72c-.78-.57-.38-1.81.59-1.81h3.46a1 1 0 0 0 .95-.69z"/></symbol></svg>

<div><span class="lbl">Five stars, clearable</span>
<div class="si">
  <fieldset>
    <legend>How was your stay at Juniper Lodge?</legend>
    <p class="si-hint" id="stay-hint">Use the arrow keys to change the rating.</p>
    <div class="si-row">
      <input type="radio" name="stay" id="stay-0" value="0" checked aria-describedby="stay-hint"><label class="si-clear" for="stay-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="stay" id="stay-1" value="1"><label class="si-star" for="stay-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1 star</span></label>
      <input type="radio" name="stay" id="stay-2" value="2"><label class="si-star" for="stay-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2 stars</span></label>
      <input type="radio" name="stay" id="stay-3" value="3"><label class="si-star" for="stay-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3 stars</span></label>
      <input type="radio" name="stay" id="stay-4" value="4"><label class="si-star" for="stay-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4 stars</span></label>
      <input type="radio" name="stay" id="stay-5" value="5"><label class="si-star" for="stay-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">5 stars</span></label>
      <p class="si-out" aria-hidden="true"><span data-v="0">No rating yet</span><span data-v="1"><b>1 of 5</b> · Poor</span><span data-v="2"><b>2 of 5</b> · Fair</span><span data-v="3"><b>3 of 5</b> · Good</span><span data-v="4"><b>4 of 5</b> · Very good</span><span data-v="5"><b>5 of 5</b> · Excellent</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Half-star steps</span>
<div class="si half">
  <fieldset>
    <legend>Rate the film: The Lantern Keeper</legend>
    <div class="si-row">
      <input type="radio" name="film" id="film-0" value="0" checked><label class="si-clear" for="film-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="film" id="film-05" value="0.5"><label class="si-star si-half l" for="film-05"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">Half a star</span></label>
      <input type="radio" name="film" id="film-1" value="1"><label class="si-star si-half r" for="film-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1 star</span></label>
      <input type="radio" name="film" id="film-15" value="1.5"><label class="si-star si-half l" for="film-15"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1.5 stars</span></label>
      <input type="radio" name="film" id="film-2" value="2"><label class="si-star si-half r" for="film-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2 stars</span></label>
      <input type="radio" name="film" id="film-25" value="2.5"><label class="si-star si-half l" for="film-25"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2.5 stars</span></label>
      <input type="radio" name="film" id="film-3" value="3"><label class="si-star si-half r" for="film-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3 stars</span></label>
      <input type="radio" name="film" id="film-35" value="3.5"><label class="si-star si-half l" for="film-35"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3.5 stars</span></label>
      <input type="radio" name="film" id="film-4" value="4"><label class="si-star si-half r" for="film-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4 stars</span></label>
      <input type="radio" name="film" id="film-45" value="4.5"><label class="si-star si-half l" for="film-45"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4.5 stars</span></label>
      <input type="radio" name="film" id="film-5" value="5"><label class="si-star si-half r" for="film-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">5 stars</span></label>
      <p class="si-out" aria-hidden="true"><span data-v="0">Not rated</span><span data-v="0.5"><b>0.5</b> / 5</span><span data-v="1"><b>1</b> / 5</span><span data-v="1.5"><b>1.5</b> / 5</span><span data-v="2"><b>2</b> / 5</span><span data-v="2.5"><b>2.5</b> / 5</span><span data-v="3"><b>3</b> / 5</span><span data-v="3.5"><b>3.5</b> / 5</span><span data-v="4"><b>4</b> / 5</span><span data-v="4.5"><b>4.5</b> / 5</span><span data-v="5"><b>5</b> / 5</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">A word under every star</span>
<div class="si si-worded">
  <fieldset>
    <legend>How was your delivery?</legend>
    <div class="si-row">
      <input type="radio" name="delivery" id="delivery-0" value="0" checked><label class="si-clear" for="delivery-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="delivery" id="delivery-1" value="1"><label class="si-star si-word" for="delivery-1"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="sr-only">1 star, </span>Poor</small></label>
      <input type="radio" name="delivery" id="delivery-2" value="2"><label class="si-star si-word" for="delivery-2"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="sr-only">2 stars, </span>Fair</small></label>
      <input type="radio" name="delivery" id="delivery-3" value="3"><label class="si-star si-word" for="delivery-3"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="sr-only">3 stars, </span>Good</small></label>
      <input type="radio" name="delivery" id="delivery-4" value="4"><label class="si-star si-word" for="delivery-4"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="sr-only">4 stars, </span>Very good</small></label>
      <input type="radio" name="delivery" id="delivery-5" value="5"><label class="si-star si-word" for="delivery-5"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="sr-only">5 stars, </span>Excellent</small></label>
    </div>
  </fieldset>
</div></div>
CSS
/* Star rating inputs: a native radio group in a fieldset, in natural 1→5 order so the arrow keys move the right way.
   The first radio ("Clear", value 0) starts checked. Every star is gold by default and the stars AFTER the checked
   radio turn grey, so the fill follows the choice with plain sibling selectors — no reversed markup, no :has(), no JS.
   Hover previews the same way: the whole row lights up, and the stars after the hovered one turn grey. */
.si{--si-on:#fbbf24;--si-on-edge:#b45309;--si-off:#f3f1ed;--si-off-edge:#878d98;--si-focus:#1d4ed8;
  --si-ink:#1f2328;--si-muted:#5b616e;--si-line:#e8e3da;--si-size:34px;
  padding:24px 28px;background:#fff;border:1px solid var(--si-line);border-radius:20px;color:var(--si-ink)}
.si fieldset{border:0;margin:0;padding:0;min-width:0}
.si legend{padding:0;font-size:1.05rem;font-weight:700}
.si-hint{margin-top:4px;font-size:.88rem;color:var(--si-muted)}
.si-row{position:relative;display:flex;flex-wrap:wrap;align-items:center;margin-top:14px}
.si-row{pointer-events:none}
.si-row label{pointer-events:auto}
.si-row input{position:absolute;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.si-star{position:relative;display:block;padding:4px;cursor:pointer;border-radius:10px}
.si-star svg{display:block;width:var(--si-size);height:var(--si-size);fill:var(--si-on);stroke:var(--si-on-edge);stroke-width:1.1;stroke-linejoin:round;
  transition:fill .15s,stroke .15s,transform .15s}
/* stars after the checked radio are empty */
.si input:checked+label~.si-star svg{fill:var(--si-off);stroke:var(--si-off-edge)}
/* hover preview (mouse and trackpad only) */
@media (hover:hover){
  .si .si-row:hover .si-star svg{fill:var(--si-on);stroke:var(--si-on-edge)}
  .si .si-row:hover label:hover~.si-star svg{fill:var(--si-off);stroke:var(--si-off-edge)}
  .si .si-star:hover svg{transform:scale(1.12)}
}
/* the chosen star gets a bar under it, so the choice is not shown by colour alone */
.si-star::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px;background:transparent}
.si input:checked+.si-star::after{background:var(--si-ink)}
.si input:focus-visible+label{outline:3px solid var(--si-focus);outline-offset:1px}
.si-clear{order:1;margin-left:10px;padding:6px 12px;border:1px solid var(--si-line);border-radius:999px;font-size:.82rem;font-weight:600;color:var(--si-muted);cursor:pointer;background:#fff}
.si-clear:hover{border-color:var(--si-off-edge);color:var(--si-ink)}
.si input:checked+.si-clear{border-style:dashed}
.si-out{order:2;flex-basis:100%;margin-top:12px;font-size:.92rem;color:var(--si-muted);min-height:1.3em}
.si-out b{color:var(--si-ink)}
.si-out span{display:none}
.si input[value="0"]:checked~.si-out [data-v="0"],.si input[value="0.5"]:checked~.si-out [data-v="0.5"],
.si input[value="1"]:checked~.si-out [data-v="1"],.si input[value="1.5"]:checked~.si-out [data-v="1.5"],
.si input[value="2"]:checked~.si-out [data-v="2"],.si input[value="2.5"]:checked~.si-out [data-v="2.5"],
.si input[value="3"]:checked~.si-out [data-v="3"],.si input[value="3.5"]:checked~.si-out [data-v="3.5"],
.si input[value="4"]:checked~.si-out [data-v="4"],.si input[value="4.5"]:checked~.si-out [data-v="4.5"],
.si input[value="5"]:checked~.si-out [data-v="5"]{display:inline}

/* half stars: each star is two labels, each showing half of the same SVG */
.si.half{--si-size:36px}
.si-half{width:calc(var(--si-size) / 2);padding:4px 0;overflow:hidden;border-radius:0}
.si-half.r{width:calc(var(--si-size) / 2 + 6px);padding-right:6px}
.si-half.r svg{margin-left:calc(var(--si-size) / -2)}
.si-half::after{display:none}
.si input:checked+.si-half{box-shadow:inset 0 -3px 0 var(--si-ink)}

/* worded scale: a word under every star */
.si-word{flex:1 1 18%;min-width:0;display:flex;flex-direction:column;align-items:center;gap:6px;padding:6px 2px 8px;text-align:center}
.si-word small{font-size:.78rem;line-height:1.2;color:var(--si-muted)}
.si input:checked+.si-word small{font-weight:700;color:var(--si-ink)}
.si-worded .si-row{max-width:520px;align-items:flex-start}
.si-worded .si-clear{margin:12px 0 0 4px}
.si-worded .si-out{display:none}
@media (max-width:420px){.si{padding:20px 18px}.si{--si-size:30px}.si.half{--si-size:32px}.si-worded{--si-size:28px}}
@media (prefers-reduced-motion:reduce){.si-star svg{transition:none}.si .si-star:hover svg{transform:none}}
.si .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><symbol id="si-star" viewBox="0 0 20 20"><path d="M9.05 2.93c.3-.92 1.6-.92 1.9 0l1.07 3.29a1 1 0 0 0 .95.69h3.46c.97 0 1.37 1.24.59 1.81l-2.8 2.03a1 1 0 0 0-.36 1.12l1.07 3.3c.3.91-.76 1.68-1.54 1.11l-2.8-2.03a1 1 0 0 0-1.18 0l-2.8 2.03c-.78.57-1.84-.2-1.54-1.11l1.07-3.3a1 1 0 0 0-.36-1.12L2.98 8.72c-.78-.57-.38-1.81.59-1.81h3.46a1 1 0 0 0 .95-.69z"/></symbol></svg>

<div><span class="lbl">Five stars, clearable</span>
<div class="si card">
  <fieldset>
    <legend>How was your stay at Juniper Lodge?</legend>
    <p class="si-hint form-text" id="stay-hint">Use the arrow keys to change the rating.</p>
    <div class="si-row">
      <input class="btn-check" type="radio" name="stay" id="stay-0" value="0" checked aria-describedby="stay-hint"><label class="si-clear btn rounded-pill" for="stay-0">Clear<span class="visually-hidden"> rating</span></label>
      <input class="btn-check" type="radio" name="stay" id="stay-1" value="1"><label class="si-star" for="stay-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">1 star</span></label>
      <input class="btn-check" type="radio" name="stay" id="stay-2" value="2"><label class="si-star" for="stay-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">2 stars</span></label>
      <input class="btn-check" type="radio" name="stay" id="stay-3" value="3"><label class="si-star" for="stay-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">3 stars</span></label>
      <input class="btn-check" type="radio" name="stay" id="stay-4" value="4"><label class="si-star" for="stay-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">4 stars</span></label>
      <input class="btn-check" type="radio" name="stay" id="stay-5" value="5"><label class="si-star" for="stay-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">5 stars</span></label>
      <p class="si-out" aria-hidden="true"><span data-v="0">No rating yet</span><span data-v="1"><b>1 of 5</b> · Poor</span><span data-v="2"><b>2 of 5</b> · Fair</span><span data-v="3"><b>3 of 5</b> · Good</span><span data-v="4"><b>4 of 5</b> · Very good</span><span data-v="5"><b>5 of 5</b> · Excellent</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Half-star steps</span>
<div class="si half card">
  <fieldset>
    <legend>Rate the film: The Lantern Keeper</legend>
    <div class="si-row">
      <input class="btn-check" type="radio" name="film" id="film-0" value="0" checked><label class="si-clear btn rounded-pill" for="film-0">Clear<span class="visually-hidden"> rating</span></label>
      <input class="btn-check" type="radio" name="film" id="film-05" value="0.5"><label class="si-star si-half l" for="film-05"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">Half a star</span></label>
      <input class="btn-check" type="radio" name="film" id="film-1" value="1"><label class="si-star si-half r" for="film-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">1 star</span></label>
      <input class="btn-check" type="radio" name="film" id="film-15" value="1.5"><label class="si-star si-half l" for="film-15"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">1.5 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-2" value="2"><label class="si-star si-half r" for="film-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">2 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-25" value="2.5"><label class="si-star si-half l" for="film-25"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">2.5 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-3" value="3"><label class="si-star si-half r" for="film-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">3 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-35" value="3.5"><label class="si-star si-half l" for="film-35"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">3.5 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-4" value="4"><label class="si-star si-half r" for="film-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">4 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-45" value="4.5"><label class="si-star si-half l" for="film-45"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">4.5 stars</span></label>
      <input class="btn-check" type="radio" name="film" id="film-5" value="5"><label class="si-star si-half r" for="film-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="visually-hidden">5 stars</span></label>
      <p class="si-out" aria-hidden="true"><span data-v="0">Not rated</span><span data-v="0.5"><b>0.5</b> / 5</span><span data-v="1"><b>1</b> / 5</span><span data-v="1.5"><b>1.5</b> / 5</span><span data-v="2"><b>2</b> / 5</span><span data-v="2.5"><b>2.5</b> / 5</span><span data-v="3"><b>3</b> / 5</span><span data-v="3.5"><b>3.5</b> / 5</span><span data-v="4"><b>4</b> / 5</span><span data-v="4.5"><b>4.5</b> / 5</span><span data-v="5"><b>5</b> / 5</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">A word under every star</span>
<div class="si si-worded card">
  <fieldset>
    <legend>How was your delivery?</legend>
    <div class="si-row">
      <input class="btn-check" type="radio" name="delivery" id="delivery-0" value="0" checked><label class="si-clear btn rounded-pill" for="delivery-0">Clear<span class="visually-hidden"> rating</span></label>
      <input class="btn-check" type="radio" name="delivery" id="delivery-1" value="1"><label class="si-star si-word" for="delivery-1"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="visually-hidden">1 star, </span>Poor</small></label>
      <input class="btn-check" type="radio" name="delivery" id="delivery-2" value="2"><label class="si-star si-word" for="delivery-2"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="visually-hidden">2 stars, </span>Fair</small></label>
      <input class="btn-check" type="radio" name="delivery" id="delivery-3" value="3"><label class="si-star si-word" for="delivery-3"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="visually-hidden">3 stars, </span>Good</small></label>
      <input class="btn-check" type="radio" name="delivery" id="delivery-4" value="4"><label class="si-star si-word" for="delivery-4"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="visually-hidden">4 stars, </span>Very good</small></label>
      <input class="btn-check" type="radio" name="delivery" id="delivery-5" value="5"><label class="si-star si-word" for="delivery-5"><svg aria-hidden="true"><use href="#si-star"/></svg><small><span class="visually-hidden">5 stars, </span>Excellent</small></label>
    </div>
  </fieldset>
</div></div>
CSS
/* Star rating inputs on Bootstrap: .btn-check radios in a fieldset inside a .card, with a .btn for Clear.
   A native radio group, in natural 1→5 order so the arrow keys move the right way.
   The first radio ("Clear", value 0) starts checked. Every star is gold by default and the stars AFTER the checked
   radio turn grey, so the fill follows the choice with plain sibling selectors — no reversed markup, no :has(), no JS.
   Hover previews the same way: the whole row lights up, and the stars after the hovered one turn grey. */
.si{--si-on:#fbbf24;--si-on-edge:#b45309;--si-off:#f3f1ed;--si-off-edge:#878d98;--si-focus:#1d4ed8;
  --si-ink:#1f2328;--si-muted:#5b616e;--si-line:#e8e3da;--si-size:34px;
  --bs-card-border-color:var(--si-line);--bs-card-border-radius:20px;--bs-card-color:var(--si-ink);
  padding:24px 28px;color:var(--si-ink)}
.si fieldset{border:0;margin:0;padding:0;min-width:0}
.si legend{float:none;margin:0;padding:0;line-height:1.3;font-size:1.05rem;font-weight:700}
.si-hint{margin:4px 0 0;--bs-secondary-color:var(--si-muted);font-size:.88rem}
.si-row{position:relative;display:flex;flex-wrap:wrap;align-items:center;margin-top:14px}
.si-row{pointer-events:none}
.si-row label{pointer-events:auto}
.si-row input{position:absolute;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.si-star{position:relative;display:block;padding:4px;cursor:pointer;border-radius:10px}
.si-star svg{display:block;width:var(--si-size);height:var(--si-size);fill:var(--si-on);stroke:var(--si-on-edge);stroke-width:1.1;stroke-linejoin:round;
  transition:fill .15s,stroke .15s,transform .15s}
/* stars after the checked radio are empty */
.si input:checked+label~.si-star svg{fill:var(--si-off);stroke:var(--si-off-edge)}
/* hover preview (mouse and trackpad only) */
@media (hover:hover){
  .si .si-row:hover .si-star svg{fill:var(--si-on);stroke:var(--si-on-edge)}
  .si .si-row:hover label:hover~.si-star svg{fill:var(--si-off);stroke:var(--si-off-edge)}
  .si .si-star:hover svg{transform:scale(1.12)}
}
/* the chosen star gets a bar under it, so the choice is not shown by colour alone */
.si-star::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px;background:transparent}
.si input:checked+.si-star::after{background:var(--si-ink)}
.si input:focus-visible+label{outline:3px solid var(--si-focus);outline-offset:1px}
.si-clear{--bs-btn-padding-x:12px;--bs-btn-padding-y:6px;--bs-btn-font-size:.82rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;
  --bs-btn-color:var(--si-muted);--bs-btn-bg:#fff;--bs-btn-border-color:var(--si-line);
  --bs-btn-hover-color:var(--si-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--si-off-edge);
  --bs-btn-active-color:var(--si-muted);--bs-btn-active-bg:#fff;--bs-btn-active-border-color:var(--si-line);order:1;margin-left:10px;transition:none}
.si .btn-check:checked+.si-clear{border-style:dashed}
.si .btn-check:focus-visible+.si-clear{box-shadow:none;outline:3px solid var(--si-focus);outline-offset:1px}
.si-out{order:2;margin-bottom:0;flex-basis:100%;margin-top:12px;font-size:.92rem;color:var(--si-muted);min-height:1.3em}
.si-out b{color:var(--si-ink)}
.si-out span{display:none}
.si input[value="0"]:checked~.si-out [data-v="0"],.si input[value="0.5"]:checked~.si-out [data-v="0.5"],
.si input[value="1"]:checked~.si-out [data-v="1"],.si input[value="1.5"]:checked~.si-out [data-v="1.5"],
.si input[value="2"]:checked~.si-out [data-v="2"],.si input[value="2.5"]:checked~.si-out [data-v="2.5"],
.si input[value="3"]:checked~.si-out [data-v="3"],.si input[value="3.5"]:checked~.si-out [data-v="3.5"],
.si input[value="4"]:checked~.si-out [data-v="4"],.si input[value="4.5"]:checked~.si-out [data-v="4.5"],
.si input[value="5"]:checked~.si-out [data-v="5"]{display:inline}

/* half stars: each star is two labels, each showing half of the same SVG */
.si.half{--si-size:36px}
.si-half{width:calc(var(--si-size) / 2);padding:4px 0;overflow:hidden;border-radius:0}
.si-half.r{width:calc(var(--si-size) / 2 + 6px);padding-right:6px}
.si-half.r svg{margin-left:calc(var(--si-size) / -2)}
.si-half::after{display:none}
.si input:checked+.si-half{box-shadow:inset 0 -3px 0 var(--si-ink)}

/* worded scale: a word under every star */
.si-word{flex:1 1 18%;min-width:0;display:flex;flex-direction:column;align-items:center;gap:6px;padding:6px 2px 8px;text-align:center}
.si-word small{font-size:.78rem;line-height:1.2;color:var(--si-muted)}
.si input:checked+.si-word small{font-weight:700;color:var(--si-ink)}
.si-worded .si-row{max-width:520px;align-items:flex-start}
.si-worded .si-clear{margin:12px 0 0 4px}
.si-worded .si-out{display:none}
@media (max-width:420px){.si{padding:20px 18px}.si{--si-size:30px}.si.half{--si-size:32px}.si-worded{--si-size:28px}}
@media (prefers-reduced-motion:reduce){.si-star svg{transition:none}.si .si-star:hover svg{transform:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><symbol id="si-star" viewBox="0 0 20 20"><path d="M9.05 2.93c.3-.92 1.6-.92 1.9 0l1.07 3.29a1 1 0 0 0 .95.69h3.46c.97 0 1.37 1.24.59 1.81l-2.8 2.03a1 1 0 0 0-.36 1.12l1.07 3.3c.3.91-.76 1.68-1.54 1.11l-2.8-2.03a1 1 0 0 0-1.18 0l-2.8 2.03c-.78.57-1.84-.2-1.54-1.11l1.07-3.3a1 1 0 0 0-.36-1.12L2.98 8.72c-.78-.57-.38-1.81.59-1.81h3.46a1 1 0 0 0 .95-.69z"/></symbol></svg>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Five stars, clearable</span>
<div class="star-rate rounded-[20px] border border-si-line bg-white px-[18px] py-5 min-[421px]:px-7 min-[421px]:py-6 text-si-ink [--si-size:30px] min-[421px]:[--si-size:34px]">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">How was your stay at Juniper Lodge?</legend>
    <p class="mt-1 text-[.88rem] text-si-muted" id="stay-hint">Use the arrow keys to change the rating.</p>
    <div data-row class="relative mt-3.5 flex flex-wrap items-center">
      <input type="radio" name="stay" id="stay-0" value="0" checked aria-describedby="stay-hint"><label data-clear class="order-1 ml-2.5 cursor-pointer rounded-full border border-si-line bg-white px-3 py-1.5 text-[.82rem] font-semibold text-si-muted hover:border-si-off-edge hover:text-si-ink" for="stay-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="stay" id="stay-1" value="1"><label data-star class="relative block cursor-pointer rounded-[10px] p-1" for="stay-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1 star</span></label>
      <input type="radio" name="stay" id="stay-2" value="2"><label data-star class="relative block cursor-pointer rounded-[10px] p-1" for="stay-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2 stars</span></label>
      <input type="radio" name="stay" id="stay-3" value="3"><label data-star class="relative block cursor-pointer rounded-[10px] p-1" for="stay-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3 stars</span></label>
      <input type="radio" name="stay" id="stay-4" value="4"><label data-star class="relative block cursor-pointer rounded-[10px] p-1" for="stay-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4 stars</span></label>
      <input type="radio" name="stay" id="stay-5" value="5"><label data-star class="relative block cursor-pointer rounded-[10px] p-1" for="stay-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">5 stars</span></label>
      <p data-out class="order-2 mt-3 min-h-[1.3em] basis-full text-[.92rem] text-si-muted" aria-hidden="true"><span data-v="0">No rating yet</span><span data-v="1"><b class="text-si-ink">1 of 5</b> · Poor</span><span data-v="2"><b class="text-si-ink">2 of 5</b> · Fair</span><span data-v="3"><b class="text-si-ink">3 of 5</b> · Good</span><span data-v="4"><b class="text-si-ink">4 of 5</b> · Very good</span><span data-v="5"><b class="text-si-ink">5 of 5</b> · Excellent</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Half-star steps</span>
<div class="star-rate rounded-[20px] border border-si-line bg-white px-[18px] py-5 min-[421px]:px-7 min-[421px]:py-6 text-si-ink [--si-size:32px] min-[421px]:[--si-size:36px]">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">Rate the film: The Lantern Keeper</legend>
    <div data-row class="relative mt-3.5 flex flex-wrap items-center">
      <input type="radio" name="film" id="film-0" value="0" checked><label data-clear class="order-1 ml-2.5 cursor-pointer rounded-full border border-si-line bg-white px-3 py-1.5 text-[.82rem] font-semibold text-si-muted hover:border-si-off-edge hover:text-si-ink" for="film-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="film" id="film-05" value="0.5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 w-[calc(var(--si-size)/2)]" data-half for="film-05"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">Half a star</span></label>
      <input type="radio" name="film" id="film-1" value="1"><label data-star class="relative block cursor-pointer overflow-hidden py-1 pr-1.5 w-[calc(var(--si-size)/2+6px)] [&>svg]:ml-[calc(var(--si-size)/-2)]" data-half for="film-1"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1 star</span></label>
      <input type="radio" name="film" id="film-15" value="1.5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 w-[calc(var(--si-size)/2)]" data-half for="film-15"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">1.5 stars</span></label>
      <input type="radio" name="film" id="film-2" value="2"><label data-star class="relative block cursor-pointer overflow-hidden py-1 pr-1.5 w-[calc(var(--si-size)/2+6px)] [&>svg]:ml-[calc(var(--si-size)/-2)]" data-half for="film-2"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2 stars</span></label>
      <input type="radio" name="film" id="film-25" value="2.5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 w-[calc(var(--si-size)/2)]" data-half for="film-25"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">2.5 stars</span></label>
      <input type="radio" name="film" id="film-3" value="3"><label data-star class="relative block cursor-pointer overflow-hidden py-1 pr-1.5 w-[calc(var(--si-size)/2+6px)] [&>svg]:ml-[calc(var(--si-size)/-2)]" data-half for="film-3"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3 stars</span></label>
      <input type="radio" name="film" id="film-35" value="3.5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 w-[calc(var(--si-size)/2)]" data-half for="film-35"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">3.5 stars</span></label>
      <input type="radio" name="film" id="film-4" value="4"><label data-star class="relative block cursor-pointer overflow-hidden py-1 pr-1.5 w-[calc(var(--si-size)/2+6px)] [&>svg]:ml-[calc(var(--si-size)/-2)]" data-half for="film-4"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4 stars</span></label>
      <input type="radio" name="film" id="film-45" value="4.5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 w-[calc(var(--si-size)/2)]" data-half for="film-45"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">4.5 stars</span></label>
      <input type="radio" name="film" id="film-5" value="5"><label data-star class="relative block cursor-pointer overflow-hidden py-1 pr-1.5 w-[calc(var(--si-size)/2+6px)] [&>svg]:ml-[calc(var(--si-size)/-2)]" data-half for="film-5"><svg aria-hidden="true"><use href="#si-star"/></svg><span class="sr-only">5 stars</span></label>
      <p data-out class="order-2 mt-3 min-h-[1.3em] basis-full text-[.92rem] text-si-muted" aria-hidden="true"><span data-v="0">Not rated</span><span data-v="0.5"><b class="text-si-ink">0.5</b> / 5</span><span data-v="1"><b class="text-si-ink">1</b> / 5</span><span data-v="1.5"><b class="text-si-ink">1.5</b> / 5</span><span data-v="2"><b class="text-si-ink">2</b> / 5</span><span data-v="2.5"><b class="text-si-ink">2.5</b> / 5</span><span data-v="3"><b class="text-si-ink">3</b> / 5</span><span data-v="3.5"><b class="text-si-ink">3.5</b> / 5</span><span data-v="4"><b class="text-si-ink">4</b> / 5</span><span data-v="4.5"><b class="text-si-ink">4.5</b> / 5</span><span data-v="5"><b class="text-si-ink">5</b> / 5</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">A word under every star</span>
<div class="star-rate rounded-[20px] border border-si-line bg-white px-[18px] py-5 min-[421px]:px-7 min-[421px]:py-6 text-si-ink [--si-size:28px] min-[421px]:[--si-size:34px]">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">How was your delivery?</legend>
    <div data-row class="relative mt-3.5 flex max-w-[520px] flex-wrap items-start">
      <input type="radio" name="delivery" id="delivery-0" value="0" checked><label data-clear class="order-1 mt-3 ml-1 cursor-pointer rounded-full border border-si-line bg-white px-3 py-1.5 text-[.82rem] font-semibold text-si-muted hover:border-si-off-edge hover:text-si-ink" for="delivery-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="delivery" id="delivery-1" value="1"><label data-star class="relative flex min-w-0 flex-[1_1_18%] cursor-pointer flex-col items-center gap-1.5 rounded-[10px] px-0.5 pt-1.5 pb-2 text-center" for="delivery-1"><svg aria-hidden="true"><use href="#si-star"/></svg><small class="text-[.78rem] leading-[1.2] text-si-muted"><span class="sr-only">1 star, </span>Poor</small></label>
      <input type="radio" name="delivery" id="delivery-2" value="2"><label data-star class="relative flex min-w-0 flex-[1_1_18%] cursor-pointer flex-col items-center gap-1.5 rounded-[10px] px-0.5 pt-1.5 pb-2 text-center" for="delivery-2"><svg aria-hidden="true"><use href="#si-star"/></svg><small class="text-[.78rem] leading-[1.2] text-si-muted"><span class="sr-only">2 stars, </span>Fair</small></label>
      <input type="radio" name="delivery" id="delivery-3" value="3"><label data-star class="relative flex min-w-0 flex-[1_1_18%] cursor-pointer flex-col items-center gap-1.5 rounded-[10px] px-0.5 pt-1.5 pb-2 text-center" for="delivery-3"><svg aria-hidden="true"><use href="#si-star"/></svg><small class="text-[.78rem] leading-[1.2] text-si-muted"><span class="sr-only">3 stars, </span>Good</small></label>
      <input type="radio" name="delivery" id="delivery-4" value="4"><label data-star class="relative flex min-w-0 flex-[1_1_18%] cursor-pointer flex-col items-center gap-1.5 rounded-[10px] px-0.5 pt-1.5 pb-2 text-center" for="delivery-4"><svg aria-hidden="true"><use href="#si-star"/></svg><small class="text-[.78rem] leading-[1.2] text-si-muted"><span class="sr-only">4 stars, </span>Very good</small></label>
      <input type="radio" name="delivery" id="delivery-5" value="5"><label data-star class="relative flex min-w-0 flex-[1_1_18%] cursor-pointer flex-col items-center gap-1.5 rounded-[10px] px-0.5 pt-1.5 pb-2 text-center" for="delivery-5"><svg aria-hidden="true"><use href="#si-star"/></svg><small class="text-[.78rem] leading-[1.2] text-si-muted"><span class="sr-only">5 stars, </span>Excellent</small></label>
    </div>
  </fieldset>
</div></div>
CSS
@theme {
  --color-si-on: #fbbf24;
  --color-si-on-edge: #b45309;
  --color-si-off: #f3f1ed;
  --color-si-off-edge: #878d98;
  --color-si-focus: #1d4ed8;
  --color-si-ink: #1f2328;
  --color-si-muted: #5b616e;
  --color-si-line: #e8e3da;
}
/* Star rating input: a native radio group in natural 1→5 order. The first radio (Clear, value 0) starts checked;
   every star is gold and the stars AFTER the checked radio turn grey, so plain sibling selectors do the fill.
   Set --si-size on the wrapper for the star size. */
@utility star-rate {
  & input { position: absolute; top: 0; left: 0; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
  & [data-row] { pointer-events: none; }
  & [data-row] label { pointer-events: auto; }
  & label[data-star] svg {
    display: block; width: var(--si-size, 34px); height: var(--si-size, 34px);
    fill: var(--color-si-on); stroke: var(--color-si-on-edge); stroke-width: 1.1; stroke-linejoin: round;
    transition: fill .15s, stroke .15s, transform .15s;
  }
  & input:checked + label ~ label[data-star] svg { fill: var(--color-si-off); stroke: var(--color-si-off-edge); }
  @media (hover: hover) {
    & [data-row]:hover label[data-star] svg { fill: var(--color-si-on); stroke: var(--color-si-on-edge); }
    & [data-row]:hover label:hover ~ label[data-star] svg { fill: var(--color-si-off); stroke: var(--color-si-off-edge); }
    & label[data-star]:hover svg { transform: scale(1.12); }
  }
  /* a bar under the chosen star, so the choice is not shown by colour alone */
  & label[data-star]:not([data-half])::after { content: ""; position: absolute; left: 50%; bottom: -4px; width: 14px; height: 3px; margin-left: -7px; border-radius: 2px; }
  & input:checked + label[data-star]:not([data-half])::after { background: var(--color-si-ink); }
  & input:checked + label[data-half] { box-shadow: inset 0 -3px 0 var(--color-si-ink); }
  & input:checked + label small { font-weight: 700; color: var(--color-si-ink); }
  & input:checked + label[data-clear] { border-style: dashed; }
  & input:focus-visible + label { outline: 3px solid var(--color-si-focus); outline-offset: 1px; }
  & [data-out] span { display: none; }
  & input[value="0"]:checked ~ [data-out] [data-v="0"], & input[value="0.5"]:checked ~ [data-out] [data-v="0.5"],
  & input[value="1"]:checked ~ [data-out] [data-v="1"], & input[value="1.5"]:checked ~ [data-out] [data-v="1.5"],
  & input[value="2"]:checked ~ [data-out] [data-v="2"], & input[value="2.5"]:checked ~ [data-out] [data-v="2.5"],
  & input[value="3"]:checked ~ [data-out] [data-v="3"], & input[value="3.5"]:checked ~ [data-out] [data-v="3.5"],
  & input[value="4"]:checked ~ [data-out] [data-v="4"], & input[value="4.5"]:checked ~ [data-out] [data-v="4.5"],
  & input[value="5"]:checked ~ [data-out] [data-v="5"] { display: inline; }
  @media (prefers-reduced-motion: reduce) {
    & label[data-star] svg { transition: none; }
    & label[data-star]:hover svg { transform: none; }
  }
}

About this set

Star rating inputs built on native radio buttons, with no JavaScript. Five stars, clearable asks how a stay at a lodge was, shows the chosen value as text such as 3 of 5 and Good, and has a Clear button. Half-star steps rates a film in ten steps from half a star to five, with each star made of two labels that each show half of the same SVG. A word under every star rates a delivery and prints Poor, Fair, Good, Very good and Excellent beneath the stars. The radios stay in their natural 1 to 5 order, so the arrow keys move the way the stars look, instead of using the common reversed-order trick that makes the keys run backwards. The first radio is the Clear option and starts checked; every star is gold by default and the stars after the checked radio turn grey, so plain sibling selectors draw the fill. Hover previews the same way: the row lights up and the stars after the hovered one go grey. The chosen star also gets a bar under it, so the choice is not shown by colour alone.

What’s included

  • Native radio group in a fieldset with a legend
  • Natural 1 to 5 order, so arrow keys match the stars
  • Clear option that is part of the radio group
  • Half-star input with ten steps
  • Hover preview and selected value written as text
  • No JavaScript and no :has()

Accessibility

Each group is a fieldset with a legend, and every radio has a name such as 3 stars or 2.5 stars. Tab moves into the group and the arrow keys change the rating in the order the stars appear. The visible star carries a 3px focus outline and the chosen star gets a bar underneath, so focus and selection do not rely on colour. Transitions and the hover zoom stop under prefers-reduced-motion.

Customise it

Set --si-on, --si-on-edge, --si-off, --si-off-edge and --si-focus on .si for colours and --si-size for the star size. Bootstrap uses the same variables with .btn-check radios; in Tailwind change the si colours in @theme and the --si-size class on the wrapper.