Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><symbol id="os-heart" viewBox="0 0 24 24"><path d="M12 20.5s-7.5-4.6-9.2-9.4C1.6 7.6 3.9 4.5 7.2 4.5c2 0 3.6 1.1 4.8 2.8 1.2-1.7 2.8-2.8 4.8-2.8 3.3 0 5.6 3.1 4.4 6.6-1.7 4.8-9.2 9.4-9.2 9.4z"/></symbol></svg>

<div><span class="lbl">Hearts</span>
<div class="os">
  <fieldset>
    <legend>How much did you enjoy this recipe?</legend>
    <p class="os-sub">Smoky aubergine and tahini flatbreads</p>
    <div class="os-row os-hearts">
      <input type="radio" name="recipe" id="recipe-0" value="0" checked><label class="os-clear" for="recipe-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="recipe" id="recipe-1" value="1"><label class="os-heart" for="recipe-1"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">1 heart</span></label>
      <input type="radio" name="recipe" id="recipe-2" value="2"><label class="os-heart" for="recipe-2"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">2 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-3" value="3"><label class="os-heart" for="recipe-3"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">3 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-4" value="4"><label class="os-heart" for="recipe-4"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">4 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-5" value="5"><label class="os-heart" for="recipe-5"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">5 hearts</span></label>
      <p class="os-out" aria-hidden="true"><span data-v="0">Not rated yet</span><span data-v="1"><b>1 of 5</b> · Not for me</span><span data-v="2"><b>2 of 5</b> · It was fine</span><span data-v="3"><b>3 of 5</b> · Liked it</span><span data-v="4"><b>4 of 5</b> · Really liked it</span><span data-v="5"><b>5 of 5</b> · Loved it</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Thumbs up or down</span>
<div class="os">
  <fieldset>
    <legend>Was this article helpful?</legend>
    <p class="os-sub">Resetting your router to factory settings</p>
    <div class="os-row os-thumbs">
      <input type="radio" name="helpful" id="helpful-yes" value="yes"><label class="os-thumb" for="helpful-yes"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>Yes</label>
      <input type="radio" name="helpful" id="helpful-no" value="no"><label class="os-thumb" for="helpful-no"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>No</label>
      <p class="os-thanks">Thanks for telling us. It helps us decide which guides to rewrite next.</p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Faces with words</span>
<div class="os">
  <fieldset>
    <legend>How satisfied are you with our support?</legend>
    <div class="os-row os-faces">
      <input type="radio" name="support" id="support-1" value="1"><label class="os-face f1" for="support-1"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M7.5 7.2l2.4 1.1M16.5 7.2l-2.4 1.1M8 17.5c1-2 2.4-3 4-3s3 1 4 3"/></svg><small>Very unhappy</small></label>
      <input type="radio" name="support" id="support-2" value="2"><label class="os-face f2" for="support-2"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 16.6c1-1 2.2-1.5 3.5-1.5s2.5.5 3.5 1.5"/></svg><small>Unhappy</small></label>
      <input type="radio" name="support" id="support-3" value="3"><label class="os-face f3" for="support-3"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 15.5h7"/></svg><small>Neutral</small></label>
      <input type="radio" name="support" id="support-4" value="4"><label class="os-face f4" for="support-4"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 14.4c1 1 2.2 1.5 3.5 1.5s2.5-.5 3.5-1.5"/></svg><small>Happy</small></label>
      <input type="radio" name="support" id="support-5" value="5"><label class="os-face f5" for="support-5"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M7.6 10.2c.4-1 1.8-1 2.2 0M14.2 10.2c.4-1 1.8-1 2.2 0"/><path d="M7.5 13.5h9a4.5 4.5 0 0 1-9 0z"/></svg><small>Very happy</small></label>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Net Promoter Score, 0 to 10</span>
<div class="os">
  <fieldset>
    <legend>How likely are you to recommend Tidewater to a friend or colleague?</legend>
    <div class="os-row os-nps">
      <input type="radio" name="nps" id="nps-0" value="0"><label for="nps-0">0<span class="sr-only">, not at all likely</span></label>
      <input type="radio" name="nps" id="nps-1" value="1"><label for="nps-1">1</label>
      <input type="radio" name="nps" id="nps-2" value="2"><label for="nps-2">2</label>
      <input type="radio" name="nps" id="nps-3" value="3"><label for="nps-3">3</label>
      <input type="radio" name="nps" id="nps-4" value="4"><label for="nps-4">4</label>
      <input type="radio" name="nps" id="nps-5" value="5"><label for="nps-5">5</label>
      <input type="radio" name="nps" id="nps-6" value="6"><label for="nps-6">6</label>
      <input type="radio" name="nps" id="nps-7" value="7"><label class="p" for="nps-7">7</label>
      <input type="radio" name="nps" id="nps-8" value="8"><label class="p" for="nps-8">8</label>
      <input type="radio" name="nps" id="nps-9" value="9"><label class="g" for="nps-9">9</label>
      <input type="radio" name="nps" id="nps-10" value="10"><label class="g" for="nps-10">10<span class="sr-only">, extremely likely</span></label>
      <div class="os-ends" aria-hidden="true"><span>0 · Not at all likely</span><span>10 · Extremely likely</span></div>
      <p class="os-out" aria-hidden="true"><span data-v="none">Pick a number from 0 to 10.</span><span data-v="0">You chose <b>0</b></span><span data-v="1">You chose <b>1</b></span><span data-v="2">You chose <b>2</b></span><span data-v="3">You chose <b>3</b></span><span data-v="4">You chose <b>4</b></span><span data-v="5">You chose <b>5</b></span><span data-v="6">You chose <b>6</b></span><span data-v="7">You chose <b>7</b></span><span data-v="8">You chose <b>8</b></span><span data-v="9">You chose <b>9</b></span><span data-v="10">You chose <b>10</b></span></p>
    </div>
  </fieldset>
</div></div>
CSS
/* Rating scales beyond stars. Every scale is a native radio group in a fieldset with a legend, so Tab enters the
   group, the arrow keys choose, and each option has a text name. The real radios are visually hidden; the label after
   each one is what you see, and it carries the focus ring. */
.os{--os-ink:#1f2328;--os-muted:#5b616e;--os-line:#e8e3da;--os-edge:#878d98;--os-focus:#1d4ed8;
  padding:24px 28px;background:#fff;border:1px solid var(--os-line);border-radius:20px;color:var(--os-ink)}
.os fieldset{border:0;margin:0;padding:0;min-width:0}
.os legend{padding:0;font-size:1.05rem;font-weight:700}
.os-sub{margin-top:4px;font-size:.88rem;color:var(--os-muted)}
.os-row{position:relative;display:flex;flex-wrap:wrap;align-items:center;margin-top:14px}
.os-row input{position:absolute;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.os-row label{cursor:pointer}
.os input:focus-visible+label{outline:3px solid var(--os-focus);outline-offset:2px}
.os .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}

/* 1 — hearts: same trick as the star input. "Clear" (value 0) starts checked; hearts after the checked radio are empty. */
.os-hearts{--on:#f43f5e;--on-edge:#be123c;--off:#f7f2f3;pointer-events:none}
.os-hearts label{pointer-events:auto}
.os-heart{position:relative;display:block;padding:4px;border-radius:10px}
.os-heart svg{display:block;width:34px;height:34px;fill:var(--on);stroke:var(--on-edge);stroke-width:1.3;stroke-linejoin:round;transition:fill .15s,stroke .15s,transform .15s}
.os-hearts input:checked+label~.os-heart svg{fill:var(--off);stroke:var(--os-edge)}
@media (hover:hover){
  .os .os-hearts:hover .os-heart svg{fill:var(--on);stroke:var(--on-edge)}
  .os .os-hearts:hover label:hover~.os-heart svg{fill:var(--off);stroke:var(--os-edge)}
  .os .os-heart:hover svg{transform:scale(1.12)}
}
.os-heart::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px}
.os-hearts input:checked+.os-heart::after{background:var(--os-ink)}
.os-clear{order:1;margin-left:10px;padding:6px 12px;border:1px solid var(--os-line);border-radius:999px;font-size:.82rem;font-weight:600;color:var(--os-muted);background:#fff}
.os-clear:hover{border-color:var(--os-edge);color:var(--os-ink)}
.os-hearts input:checked+.os-clear{border-style:dashed}
.os-out{order:2;flex-basis:100%;margin-top:12px;min-height:1.3em;font-size:.92rem;color:var(--os-muted)}
.os-out b{color:var(--os-ink)}
.os-out span{display:none}
.os input[value="0"]:checked~.os-out [data-v="0"],.os input[value="1"]:checked~.os-out [data-v="1"],
.os input[value="2"]:checked~.os-out [data-v="2"],.os input[value="3"]:checked~.os-out [data-v="3"],
.os input[value="4"]:checked~.os-out [data-v="4"],.os input[value="5"]:checked~.os-out [data-v="5"]{display:inline}

/* 2 — thumbs: two radios drawn as buttons; the chosen one turns solid and its icon fills in */
.os-thumbs{gap:10px}
.os-thumb{display:inline-flex;align-items:center;gap:9px;padding:10px 18px 10px 14px;border:1.5px solid var(--os-edge);border-radius:999px;
  font-size:.95rem;font-weight:600;background:#fff;transition:background .15s,color .15s,border-color .15s}
.os-thumb svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.os-thumb:hover{border-color:var(--os-ink)}
.os-thumbs input:checked+.os-thumb{background:var(--os-ink);border-color:var(--os-ink);color:#fff}
.os-thumbs input:checked+.os-thumb svg{fill:currentColor;fill-opacity:.35}
.os-thanks{display:none;flex-basis:100%;margin-top:4px;font-size:.9rem;color:var(--os-muted)}
.os-thumbs input:checked~.os-thanks{display:block}

/* 3 — faces: one face per option with its word underneath; the chosen face takes its own colour, rises and goes bold */
.os-faces{flex-wrap:nowrap;gap:6px;align-items:stretch}
.os-face{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;gap:8px;padding:12px 4px 10px;border:1.5px solid transparent;border-radius:14px;
  text-align:center;color:var(--os-muted);transition:transform .15s,background .15s,border-color .15s}
.os-face svg{width:40px;height:40px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.os-face svg .eye{fill:currentColor;stroke:none}
.os-face small{font-size:.8rem;line-height:1.2}
.os-face:hover{background:#f6f4f0}
.os-faces input:checked+.os-face{color:var(--tone);background:var(--tint);border-color:var(--tone);transform:translateY(-3px)}
.os-faces input:checked+.os-face small{font-weight:700;color:var(--os-ink)}
.os-face.f1{--tone:#b91c1c;--tint:#fef2f2}.os-face.f2{--tone:#c2410c;--tint:#fff7ed}.os-face.f3{--tone:#a16207;--tint:#fefce8}
.os-face.f4{--tone:#4d7c0f;--tint:#f7fee7}.os-face.f5{--tone:#15803d;--tint:#f0fdf4}

/* 4 — NPS: eleven numbered tiles, 0 to 10, with the scale's ends written out */
.os-nps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:6px}
.os-nps label{display:grid;place-items:center;height:44px;border:1.5px solid var(--os-edge);border-radius:10px;font-weight:700;font-variant-numeric:tabular-nums;
  background:#fff;transition:background .15s,color .15s,border-color .15s}
.os-nps label:hover{border-color:var(--os-ink);background:#f6f4f0}
.os-nps input:checked+label{color:#fff;background:var(--band,#b91c1c);border-color:var(--band,#b91c1c);box-shadow:0 0 0 2px #fff,0 0 0 4px var(--os-ink)}
.os-nps .p{--band:#a35204}.os-nps .g{--band:#15803d}
.os-ends{grid-column:1/-1;display:flex;justify-content:space-between;gap:12px;margin-top:4px;font-size:.82rem;color:var(--os-muted)}
.os-nps .os-out{grid-column:1/-1;margin-top:8px}
.os-nps input:checked~.os-out [data-v="none"]{display:none}
.os-nps .os-out [data-v="none"]{display:inline}
.os input[value="6"]:checked~.os-out [data-v="6"],.os input[value="7"]:checked~.os-out [data-v="7"],.os input[value="8"]:checked~.os-out [data-v="8"],
.os input[value="9"]:checked~.os-out [data-v="9"],.os input[value="10"]:checked~.os-out [data-v="10"]{display:inline}
@media (max-width:480px){.os{padding:20px 18px}.os-nps{grid-template-columns:repeat(6,minmax(0,1fr))}.os-face svg{width:34px;height:34px}.os-faces{gap:2px}}
@media (prefers-reduced-motion:reduce){.os-heart svg,.os-thumb,.os-face,.os-nps label{transition:none}.os .os-heart:hover svg,.os-faces input:checked+.os-face{transform:none}}

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="os-heart" viewBox="0 0 24 24"><path d="M12 20.5s-7.5-4.6-9.2-9.4C1.6 7.6 3.9 4.5 7.2 4.5c2 0 3.6 1.1 4.8 2.8 1.2-1.7 2.8-2.8 4.8-2.8 3.3 0 5.6 3.1 4.4 6.6-1.7 4.8-9.2 9.4-9.2 9.4z"/></symbol></svg>

<div><span class="lbl">Hearts</span>
<div class="os card">
  <fieldset>
    <legend>How much did you enjoy this recipe?</legend>
    <p class="os-sub">Smoky aubergine and tahini flatbreads</p>
    <div class="os-row os-hearts">
      <input class="btn-check" type="radio" name="recipe" id="recipe-0" value="0" checked><label class="os-clear btn rounded-pill" for="recipe-0">Clear<span class="visually-hidden"> rating</span></label>
      <input class="btn-check" type="radio" name="recipe" id="recipe-1" value="1"><label class="os-heart" for="recipe-1"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="visually-hidden">1 heart</span></label>
      <input class="btn-check" type="radio" name="recipe" id="recipe-2" value="2"><label class="os-heart" for="recipe-2"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="visually-hidden">2 hearts</span></label>
      <input class="btn-check" type="radio" name="recipe" id="recipe-3" value="3"><label class="os-heart" for="recipe-3"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="visually-hidden">3 hearts</span></label>
      <input class="btn-check" type="radio" name="recipe" id="recipe-4" value="4"><label class="os-heart" for="recipe-4"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="visually-hidden">4 hearts</span></label>
      <input class="btn-check" type="radio" name="recipe" id="recipe-5" value="5"><label class="os-heart" for="recipe-5"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="visually-hidden">5 hearts</span></label>
      <p class="os-out" aria-hidden="true"><span data-v="0">Not rated yet</span><span data-v="1"><b>1 of 5</b> · Not for me</span><span data-v="2"><b>2 of 5</b> · It was fine</span><span data-v="3"><b>3 of 5</b> · Liked it</span><span data-v="4"><b>4 of 5</b> · Really liked it</span><span data-v="5"><b>5 of 5</b> · Loved it</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Thumbs up or down</span>
<div class="os card">
  <fieldset>
    <legend>Was this article helpful?</legend>
    <p class="os-sub">Resetting your router to factory settings</p>
    <div class="os-row os-thumbs">
      <input class="btn-check" type="radio" name="helpful" id="helpful-yes" value="yes"><label class="os-thumb btn" for="helpful-yes"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>Yes</label>
      <input class="btn-check" type="radio" name="helpful" id="helpful-no" value="no"><label class="os-thumb btn" for="helpful-no"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>No</label>
      <p class="os-thanks">Thanks for telling us. It helps us decide which guides to rewrite next.</p>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Faces with words</span>
<div class="os card">
  <fieldset>
    <legend>How satisfied are you with our support?</legend>
    <div class="os-row os-faces">
      <input class="btn-check" type="radio" name="support" id="support-1" value="1"><label class="os-face f1" for="support-1"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M7.5 7.2l2.4 1.1M16.5 7.2l-2.4 1.1M8 17.5c1-2 2.4-3 4-3s3 1 4 3"/></svg><small>Very unhappy</small></label>
      <input class="btn-check" type="radio" name="support" id="support-2" value="2"><label class="os-face f2" for="support-2"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 16.6c1-1 2.2-1.5 3.5-1.5s2.5.5 3.5 1.5"/></svg><small>Unhappy</small></label>
      <input class="btn-check" type="radio" name="support" id="support-3" value="3"><label class="os-face f3" for="support-3"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 15.5h7"/></svg><small>Neutral</small></label>
      <input class="btn-check" type="radio" name="support" id="support-4" value="4"><label class="os-face f4" for="support-4"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="eye" cx="8.6" cy="9.8" r="1.2"/><circle class="eye" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 14.4c1 1 2.2 1.5 3.5 1.5s2.5-.5 3.5-1.5"/></svg><small>Happy</small></label>
      <input class="btn-check" type="radio" name="support" id="support-5" value="5"><label class="os-face f5" for="support-5"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M7.6 10.2c.4-1 1.8-1 2.2 0M14.2 10.2c.4-1 1.8-1 2.2 0"/><path d="M7.5 13.5h9a4.5 4.5 0 0 1-9 0z"/></svg><small>Very happy</small></label>
    </div>
  </fieldset>
</div></div>

<div><span class="lbl">Net Promoter Score, 0 to 10</span>
<div class="os card">
  <fieldset>
    <legend>How likely are you to recommend Tidewater to a friend or colleague?</legend>
    <div class="os-row os-nps">
      <input class="btn-check" type="radio" name="nps" id="nps-0" value="0"><label class="btn" for="nps-0">0<span class="visually-hidden">, not at all likely</span></label>
      <input class="btn-check" type="radio" name="nps" id="nps-1" value="1"><label class="btn" for="nps-1">1</label>
      <input class="btn-check" type="radio" name="nps" id="nps-2" value="2"><label class="btn" for="nps-2">2</label>
      <input class="btn-check" type="radio" name="nps" id="nps-3" value="3"><label class="btn" for="nps-3">3</label>
      <input class="btn-check" type="radio" name="nps" id="nps-4" value="4"><label class="btn" for="nps-4">4</label>
      <input class="btn-check" type="radio" name="nps" id="nps-5" value="5"><label class="btn" for="nps-5">5</label>
      <input class="btn-check" type="radio" name="nps" id="nps-6" value="6"><label class="btn" for="nps-6">6</label>
      <input class="btn-check" type="radio" name="nps" id="nps-7" value="7"><label class="btn p" for="nps-7">7</label>
      <input class="btn-check" type="radio" name="nps" id="nps-8" value="8"><label class="btn p" for="nps-8">8</label>
      <input class="btn-check" type="radio" name="nps" id="nps-9" value="9"><label class="btn g" for="nps-9">9</label>
      <input class="btn-check" type="radio" name="nps" id="nps-10" value="10"><label class="btn g" for="nps-10">10<span class="visually-hidden">, extremely likely</span></label>
      <div class="os-ends" aria-hidden="true"><span>0 · Not at all likely</span><span>10 · Extremely likely</span></div>
      <p class="os-out" aria-hidden="true"><span data-v="none">Pick a number from 0 to 10.</span><span data-v="0">You chose <b>0</b></span><span data-v="1">You chose <b>1</b></span><span data-v="2">You chose <b>2</b></span><span data-v="3">You chose <b>3</b></span><span data-v="4">You chose <b>4</b></span><span data-v="5">You chose <b>5</b></span><span data-v="6">You chose <b>6</b></span><span data-v="7">You chose <b>7</b></span><span data-v="8">You chose <b>8</b></span><span data-v="9">You chose <b>9</b></span><span data-v="10">You chose <b>10</b></span></p>
    </div>
  </fieldset>
</div></div>
CSS
/* Rating scales beyond stars on Bootstrap: .btn-check radios inside a .card, with .btn for the clear, thumb and NPS buttons. Every scale is a native radio group in a fieldset with a legend, so Tab enters the
   group, the arrow keys choose, and each option has a text name. The real radios are visually hidden; the label after
   each one is what you see, and it carries the focus ring. */
.os{--os-ink:#1f2328;--os-muted:#5b616e;--os-line:#e8e3da;--os-edge:#878d98;--os-focus:#1d4ed8;
  --bs-card-border-color:var(--os-line);--bs-card-border-radius:20px;--bs-card-color:var(--os-ink);padding:24px 28px;color:var(--os-ink)}
.os p{margin-bottom:0}
.os fieldset{border:0;margin:0;padding:0;min-width:0}
.os legend{float:none;margin:0;padding:0;line-height:1.3;font-size:1.05rem;font-weight:700}
.os-sub{margin-top:4px;font-size:.88rem;color:var(--os-muted)}
.os-row{position:relative;display:flex;flex-wrap:wrap;align-items:center;margin-top:14px}
.os-row .btn-check{position:absolute;clip:auto;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.os-row label{cursor:pointer}
.os input:focus-visible+label,.os .btn-check:focus-visible+.btn{outline:3px solid var(--os-focus);outline-offset:2px}

/* 1 — hearts: same trick as the star input. "Clear" (value 0) starts checked; hearts after the checked radio are empty. */
.os-hearts{--on:#f43f5e;--on-edge:#be123c;--off:#f7f2f3;pointer-events:none}
.os-hearts label{pointer-events:auto}
.os-heart{position:relative;display:block;padding:4px;border-radius:10px}
.os-heart svg{display:block;width:34px;height:34px;fill:var(--on);stroke:var(--on-edge);stroke-width:1.3;stroke-linejoin:round;transition:fill .15s,stroke .15s,transform .15s}
.os-hearts input:checked+label~.os-heart svg{fill:var(--off);stroke:var(--os-edge)}
@media (hover:hover){
  .os .os-hearts:hover .os-heart svg{fill:var(--on);stroke:var(--on-edge)}
  .os .os-hearts:hover label:hover~.os-heart svg{fill:var(--off);stroke:var(--os-edge)}
  .os .os-heart:hover svg{transform:scale(1.12)}
}
.os-heart::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px}
.os-hearts input:checked+.os-heart::after{background:var(--os-ink)}
.os-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(--os-muted);--bs-btn-bg:#fff;--bs-btn-border-color:var(--os-line);--bs-btn-hover-color:var(--os-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--os-edge);
  --bs-btn-active-color:var(--os-muted);--bs-btn-active-bg:#fff;--bs-btn-active-border-color:var(--os-line);order:1;margin-left:10px;transition:none}
.os-hearts .btn-check:checked+.os-clear{border-style:dashed}
.os-out{order:2;flex-basis:100%;margin-top:12px;min-height:1.3em;font-size:.92rem;color:var(--os-muted)}
.os-out b{color:var(--os-ink)}
.os-out span{display:none}
.os input[value="0"]:checked~.os-out [data-v="0"],.os input[value="1"]:checked~.os-out [data-v="1"],
.os input[value="2"]:checked~.os-out [data-v="2"],.os input[value="3"]:checked~.os-out [data-v="3"],
.os input[value="4"]:checked~.os-out [data-v="4"],.os input[value="5"]:checked~.os-out [data-v="5"]{display:inline}

/* 2 — thumbs: two radios drawn as buttons; the chosen one turns solid and its icon fills in */
.os-thumbs{gap:10px}
.os-thumb{--bs-btn-padding-x:18px;--bs-btn-padding-y:10px;--bs-btn-font-size:.95rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-border-width:1.5px;--bs-btn-border-radius:999px;
  --bs-btn-color:var(--os-ink);--bs-btn-bg:#fff;--bs-btn-border-color:var(--os-edge);--bs-btn-hover-color:var(--os-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--os-ink);
  --bs-btn-active-color:#fff;--bs-btn-active-bg:var(--os-ink);--bs-btn-active-border-color:var(--os-ink);--bs-btn-focus-box-shadow:none;
  display:inline-flex;align-items:center;gap:9px;padding-left:14px;transition:background-color .15s,color .15s,border-color .15s}
.os-thumb svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.os-thumbs input:checked+.os-thumb{background:var(--os-ink);border-color:var(--os-ink);color:#fff}
.os-thumbs input:checked+.os-thumb svg{fill:currentColor;fill-opacity:.35}
.os-thanks{display:none;flex-basis:100%;margin-top:4px;font-size:.9rem;color:var(--os-muted)}
.os-thumbs input:checked~.os-thanks{display:block}

/* 3 — faces: one face per option with its word underneath; the chosen face takes its own colour, rises and goes bold */
.os-faces{flex-wrap:nowrap;gap:6px;align-items:stretch}
.os-face{flex:1 1 0;min-width:0;display:flex;flex-direction:column;align-items:center;gap:8px;padding:12px 4px 10px;border:1.5px solid transparent;border-radius:14px;
  text-align:center;color:var(--os-muted);transition:transform .15s,background .15s,border-color .15s}
.os-face svg{width:40px;height:40px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
.os-face svg .eye{fill:currentColor;stroke:none}
.os-face small{font-size:.8rem;line-height:1.2}
.os-face:hover{background:#f6f4f0}
.os-faces input:checked+.os-face{color:var(--tone);background:var(--tint);border-color:var(--tone);transform:translateY(-3px)}
.os-faces input:checked+.os-face small{font-weight:700;color:var(--os-ink)}
.os-face.f1{--tone:#b91c1c;--tint:#fef2f2}.os-face.f2{--tone:#c2410c;--tint:#fff7ed}.os-face.f3{--tone:#a16207;--tint:#fefce8}
.os-face.f4{--tone:#4d7c0f;--tint:#f7fee7}.os-face.f5{--tone:#15803d;--tint:#f0fdf4}

/* 4 — NPS: eleven numbered tiles, 0 to 10, with the scale's ends written out */
.os-nps{display:grid;grid-template-columns:repeat(11,minmax(0,1fr));gap:6px}
.os-nps .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-font-weight:700;--bs-btn-color:var(--os-ink);--bs-btn-border-width:1.5px;--bs-btn-border-radius:10px;--bs-btn-focus-box-shadow:none;
  display:grid;place-items:center;height:44px;border:1.5px solid var(--os-edge);border-radius:10px;font-weight:700;font-variant-numeric:tabular-nums;
  background:#fff;transition:background .15s,color .15s,border-color .15s}
.os-nps .btn:hover{color:var(--os-ink);border-color:var(--os-ink);background:#f6f4f0}
.os .os-nps .btn-check:checked+.btn{color:#fff;background:var(--band,#b91c1c);border-color:var(--band,#b91c1c);box-shadow:0 0 0 2px #fff,0 0 0 4px var(--os-ink)}
.os-nps .p{--band:#a35204}.os-nps .g{--band:#15803d}
.os-ends{grid-column:1/-1;display:flex;justify-content:space-between;gap:12px;margin-top:4px;font-size:.82rem;color:var(--os-muted)}
.os-nps .os-out{grid-column:1/-1;margin-top:8px}
.os-nps input:checked~.os-out [data-v="none"]{display:none}
.os-nps .os-out [data-v="none"]{display:inline}
.os input[value="6"]:checked~.os-out [data-v="6"],.os input[value="7"]:checked~.os-out [data-v="7"],.os input[value="8"]:checked~.os-out [data-v="8"],
.os input[value="9"]:checked~.os-out [data-v="9"],.os input[value="10"]:checked~.os-out [data-v="10"]{display:inline}
@media (max-width:480px){.os{padding:20px 18px}.os-nps{grid-template-columns:repeat(6,minmax(0,1fr))}.os-face svg{width:34px;height:34px}.os-faces{gap:2px}}
@media (prefers-reduced-motion:reduce){.os-heart svg,.os-thumb,.os-face,.os-nps label{transition:none}.os .os-heart:hover svg,.os-faces input:checked+.os-face{transform:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" class="absolute" aria-hidden="true" focusable="false"><symbol id="os-heart" viewBox="0 0 24 24"><path d="M12 20.5s-7.5-4.6-9.2-9.4C1.6 7.6 3.9 4.5 7.2 4.5c2 0 3.6 1.1 4.8 2.8 1.2-1.7 2.8-2.8 4.8-2.8 3.3 0 5.6 3.1 4.4 6.6-1.7 4.8-9.2 9.4-9.2 9.4z"/></symbol></svg>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Hearts</span>
<div class="rounded-[20px] border border-os-line bg-white px-[18px] py-5 min-[481px]:px-7 min-[481px]:py-6 text-os-ink">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">How much did you enjoy this recipe?</legend>
    <p class="mt-1 text-[.88rem] text-os-muted">Smoky aubergine and tahini flatbreads</p>
    <div class="heart-rate relative mt-3.5 flex flex-wrap items-center">
      <input type="radio" name="recipe" id="recipe-0" value="0" checked><label data-clear class="order-1 ml-2.5 cursor-pointer rounded-full border border-os-line bg-white px-3 py-1.5 text-[.82rem] font-semibold text-os-muted hover:border-os-edge hover:text-os-ink" for="recipe-0">Clear<span class="sr-only"> rating</span></label>
      <input type="radio" name="recipe" id="recipe-1" value="1"><label data-heart class="relative block cursor-pointer rounded-[10px] p-1" for="recipe-1"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">1 heart</span></label>
      <input type="radio" name="recipe" id="recipe-2" value="2"><label data-heart class="relative block cursor-pointer rounded-[10px] p-1" for="recipe-2"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">2 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-3" value="3"><label data-heart class="relative block cursor-pointer rounded-[10px] p-1" for="recipe-3"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">3 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-4" value="4"><label data-heart class="relative block cursor-pointer rounded-[10px] p-1" for="recipe-4"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">4 hearts</span></label>
      <input type="radio" name="recipe" id="recipe-5" value="5"><label data-heart class="relative block cursor-pointer rounded-[10px] p-1" for="recipe-5"><svg aria-hidden="true"><use href="#os-heart"/></svg><span class="sr-only">5 hearts</span></label>
      <p data-out class="rate-out order-2 mt-3 min-h-[1.3em] basis-full text-[.92rem] text-os-muted [&_b]:text-os-ink" aria-hidden="true"><span data-v="0">Not rated yet</span><span data-v="1"><b>1 of 5</b> · Not for me</span><span data-v="2"><b>2 of 5</b> · It was fine</span><span data-v="3"><b>3 of 5</b> · Liked it</span><span data-v="4"><b>4 of 5</b> · Really liked it</span><span data-v="5"><b>5 of 5</b> · Loved it</span></p>
    </div>
  </fieldset>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Thumbs up or down</span>
<div class="rounded-[20px] border border-os-line bg-white px-[18px] py-5 min-[481px]:px-7 min-[481px]:py-6 text-os-ink">
  <fieldset class="group m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">Was this article helpful?</legend>
    <p class="mt-1 text-[.88rem] text-os-muted">Resetting your router to factory settings</p>
    <div class="mt-3.5 flex flex-wrap items-center gap-2.5">
      <div class="relative"><input class="peer sr-only" type="radio" name="helpful" id="helpful-yes" value="yes"><label class="inline-flex cursor-pointer items-center gap-[9px] rounded-full border-[1.5px] border-os-edge bg-white py-2.5 pr-[18px] pl-3.5 text-[.95rem] font-semibold transition-colors hover:border-os-ink motion-reduce:transition-none [&_svg]:size-5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.8] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:border-os-ink peer-checked:bg-os-ink peer-checked:text-white peer-checked:[&_svg]:fill-current/35 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="helpful-yes"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>Yes</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="helpful" id="helpful-no" value="no"><label class="inline-flex cursor-pointer items-center gap-[9px] rounded-full border-[1.5px] border-os-edge bg-white py-2.5 pr-[18px] pl-3.5 text-[.95rem] font-semibold transition-colors hover:border-os-ink motion-reduce:transition-none [&_svg]:size-5 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.8] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:border-os-ink peer-checked:bg-os-ink peer-checked:text-white peer-checked:[&_svg]:fill-current/35 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="helpful-no"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>No</label></div>
      <p class="mt-1 hidden basis-full text-[.9rem] text-os-muted group-has-[:checked]:block">Thanks for telling us. It helps us decide which guides to rewrite next.</p>
    </div>
  </fieldset>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Faces with words</span>
<div class="rounded-[20px] border border-os-line bg-white px-[18px] py-5 min-[481px]:px-7 min-[481px]:py-6 text-os-ink">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">How satisfied are you with our support?</legend>
    <div class="mt-3.5 flex gap-0.5 min-[481px]:gap-1.5">
      <div class="relative min-w-0 flex-1"><input class="peer sr-only" type="radio" name="support" id="support-1" value="1"><label class="flex h-full cursor-pointer flex-col items-center gap-2 rounded-[14px] border-[1.5px] border-transparent px-1 pt-3 pb-2.5 text-center text-os-muted transition hover:bg-[#f6f4f0] motion-reduce:transition-none [&_svg]:size-[34px] min-[481px]:[&_svg]:size-10 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.5] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:-translate-y-[3px] motion-reduce:peer-checked:translate-y-0 peer-checked:border-[#b91c1c] peer-checked:bg-[#fef2f2] peer-checked:text-[#b91c1c] peer-checked:[&_small]:font-bold peer-checked:[&_small]:text-os-ink peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="support-1"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="fill-current stroke-none" cx="8.6" cy="9.8" r="1.2"/><circle class="fill-current stroke-none" cx="15.4" cy="9.8" r="1.2"/><path d="M7.5 7.2l2.4 1.1M16.5 7.2l-2.4 1.1M8 17.5c1-2 2.4-3 4-3s3 1 4 3"/></svg><small class="text-[.8rem] leading-[1.2]">Very unhappy</small></label></div>
      <div class="relative min-w-0 flex-1"><input class="peer sr-only" type="radio" name="support" id="support-2" value="2"><label class="flex h-full cursor-pointer flex-col items-center gap-2 rounded-[14px] border-[1.5px] border-transparent px-1 pt-3 pb-2.5 text-center text-os-muted transition hover:bg-[#f6f4f0] motion-reduce:transition-none [&_svg]:size-[34px] min-[481px]:[&_svg]:size-10 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.5] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:-translate-y-[3px] motion-reduce:peer-checked:translate-y-0 peer-checked:border-[#c2410c] peer-checked:bg-[#fff7ed] peer-checked:text-[#c2410c] peer-checked:[&_small]:font-bold peer-checked:[&_small]:text-os-ink peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="support-2"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="fill-current stroke-none" cx="8.6" cy="9.8" r="1.2"/><circle class="fill-current stroke-none" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 16.6c1-1 2.2-1.5 3.5-1.5s2.5.5 3.5 1.5"/></svg><small class="text-[.8rem] leading-[1.2]">Unhappy</small></label></div>
      <div class="relative min-w-0 flex-1"><input class="peer sr-only" type="radio" name="support" id="support-3" value="3"><label class="flex h-full cursor-pointer flex-col items-center gap-2 rounded-[14px] border-[1.5px] border-transparent px-1 pt-3 pb-2.5 text-center text-os-muted transition hover:bg-[#f6f4f0] motion-reduce:transition-none [&_svg]:size-[34px] min-[481px]:[&_svg]:size-10 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.5] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:-translate-y-[3px] motion-reduce:peer-checked:translate-y-0 peer-checked:border-[#a16207] peer-checked:bg-[#fefce8] peer-checked:text-[#a16207] peer-checked:[&_small]:font-bold peer-checked:[&_small]:text-os-ink peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="support-3"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="fill-current stroke-none" cx="8.6" cy="9.8" r="1.2"/><circle class="fill-current stroke-none" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 15.5h7"/></svg><small class="text-[.8rem] leading-[1.2]">Neutral</small></label></div>
      <div class="relative min-w-0 flex-1"><input class="peer sr-only" type="radio" name="support" id="support-4" value="4"><label class="flex h-full cursor-pointer flex-col items-center gap-2 rounded-[14px] border-[1.5px] border-transparent px-1 pt-3 pb-2.5 text-center text-os-muted transition hover:bg-[#f6f4f0] motion-reduce:transition-none [&_svg]:size-[34px] min-[481px]:[&_svg]:size-10 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.5] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:-translate-y-[3px] motion-reduce:peer-checked:translate-y-0 peer-checked:border-[#4d7c0f] peer-checked:bg-[#f7fee7] peer-checked:text-[#4d7c0f] peer-checked:[&_small]:font-bold peer-checked:[&_small]:text-os-ink peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="support-4"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle class="fill-current stroke-none" cx="8.6" cy="9.8" r="1.2"/><circle class="fill-current stroke-none" cx="15.4" cy="9.8" r="1.2"/><path d="M8.5 14.4c1 1 2.2 1.5 3.5 1.5s2.5-.5 3.5-1.5"/></svg><small class="text-[.8rem] leading-[1.2]">Happy</small></label></div>
      <div class="relative min-w-0 flex-1"><input class="peer sr-only" type="radio" name="support" id="support-5" value="5"><label class="flex h-full cursor-pointer flex-col items-center gap-2 rounded-[14px] border-[1.5px] border-transparent px-1 pt-3 pb-2.5 text-center text-os-muted transition hover:bg-[#f6f4f0] motion-reduce:transition-none [&_svg]:size-[34px] min-[481px]:[&_svg]:size-10 [&_svg]:fill-none [&_svg]:stroke-current [&_svg]:stroke-[1.5] [&_svg]:[stroke-linecap:round] [&_svg]:[stroke-linejoin:round] peer-checked:-translate-y-[3px] motion-reduce:peer-checked:translate-y-0 peer-checked:border-[#15803d] peer-checked:bg-[#f0fdf4] peer-checked:text-[#15803d] peer-checked:[&_small]:font-bold peer-checked:[&_small]:text-os-ink peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="support-5"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M7.6 10.2c.4-1 1.8-1 2.2 0M14.2 10.2c.4-1 1.8-1 2.2 0"/><path d="M7.5 13.5h9a4.5 4.5 0 0 1-9 0z"/></svg><small class="text-[.8rem] leading-[1.2]">Very happy</small></label></div>
    </div>
  </fieldset>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5d6573] uppercase">Net Promoter Score, 0 to 10</span>
<div class="rounded-[20px] border border-os-line bg-white px-[18px] py-5 min-[481px]:px-7 min-[481px]:py-6 text-os-ink">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">How likely are you to recommend Tidewater to a friend or colleague?</legend>
    <div class="mt-3.5 grid grid-cols-6 gap-1.5 min-[481px]:grid-cols-11">
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-0" value="0"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-0">0<span class="sr-only">, not at all likely</span></label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-1" value="1"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-1">1</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-2" value="2"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-2">2</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-3" value="3"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-3">3</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-4" value="4"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-4">4</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-5" value="5"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-5">5</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-6" value="6"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#b91c1c] peer-checked:bg-[#b91c1c] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-6">6</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-7" value="7"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#a35204] peer-checked:bg-[#a35204] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-7">7</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-8" value="8"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#a35204] peer-checked:bg-[#a35204] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-8">8</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-9" value="9"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#15803d] peer-checked:bg-[#15803d] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-9">9</label></div>
      <div class="relative"><input class="peer sr-only" type="radio" name="nps" id="nps-10" value="10"><label class="grid h-11 cursor-pointer place-items-center rounded-[10px] border-[1.5px] border-os-edge bg-white font-bold tabular-nums transition-colors hover:border-os-ink hover:bg-[#f6f4f0] motion-reduce:transition-none peer-checked:border-[#15803d] peer-checked:bg-[#15803d] peer-checked:text-white peer-checked:shadow-[0_0_0_2px_#fff,0_0_0_4px_#1f2328] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-os-focus" for="nps-10">10<span class="sr-only">, extremely likely</span></label></div>
      <div class="col-span-full mt-1 flex justify-between gap-3 text-[.82rem] text-os-muted" aria-hidden="true"><span>0 · Not at all likely</span><span>10 · Extremely likely</span></div>
      <p data-out class="rate-out col-span-full mt-2 text-[.92rem] text-os-muted [&_b]:text-os-ink" aria-hidden="true"><span data-v="none">Pick a number from 0 to 10.</span><span data-v="0">You chose <b>0</b></span><span data-v="1">You chose <b>1</b></span><span data-v="2">You chose <b>2</b></span><span data-v="3">You chose <b>3</b></span><span data-v="4">You chose <b>4</b></span><span data-v="5">You chose <b>5</b></span><span data-v="6">You chose <b>6</b></span><span data-v="7">You chose <b>7</b></span><span data-v="8">You chose <b>8</b></span><span data-v="9">You chose <b>9</b></span><span data-v="10">You chose <b>10</b></span></p>
    </div>
  </fieldset>
</div></div>
CSS
@theme {
  --color-os-ink: #1f2328;
  --color-os-muted: #5b616e;
  --color-os-line: #e8e3da;
  --color-os-edge: #878d98;
  --color-os-focus: #1d4ed8;
  --color-os-heart: #f43f5e;
  --color-os-heart-edge: #be123c;
  --color-os-heart-off: #f7f2f3;
}
/* Hearts: a native radio group in natural 1→5 order. "Clear" (value 0) starts checked; every heart is filled
   and the hearts AFTER the checked radio turn empty, so plain sibling selectors do the fill. */
@utility heart-rate {
  pointer-events: none;
  & label { pointer-events: auto; }
  & input { position: absolute; top: 0; left: 0; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
  & label[data-heart] svg { display: block; width: 34px; height: 34px; fill: var(--color-os-heart); stroke: var(--color-os-heart-edge); stroke-width: 1.3; stroke-linejoin: round; transition: fill .15s, stroke .15s, transform .15s; }
  & input:checked + label ~ label[data-heart] svg { fill: var(--color-os-heart-off); stroke: var(--color-os-edge); }
  @media (hover: hover) {
    &:hover label[data-heart] svg { fill: var(--color-os-heart); stroke: var(--color-os-heart-edge); }
    &:hover label:hover ~ label[data-heart] svg { fill: var(--color-os-heart-off); stroke: var(--color-os-edge); }
    & label[data-heart]:hover svg { transform: scale(1.12); }
  }
  & label[data-heart]::after { content: ""; position: absolute; left: 50%; bottom: -4px; width: 14px; height: 3px; margin-left: -7px; border-radius: 2px; }
  & input:checked + label[data-heart]::after { background: var(--color-os-ink); }
  & input:checked + label[data-clear] { border-style: dashed; }
  & input:focus-visible + label { outline: 3px solid var(--color-os-focus); outline-offset: 2px; }
  @media (prefers-reduced-motion: reduce) { & label[data-heart] svg { transition: none; } & label[data-heart]:hover svg { transform: none; } }
}
/* Text that follows the choice: show the span whose data-v matches the checked value (Tailwind 4 browsers all support :has()). */
@utility rate-out {
  & span { display: none; }
  & span[data-v="none"] { display: inline; }
}
/* unlayered, so these win over the rate-out utility */
fieldset:has(input:checked) [data-out] span[data-v="none"] { display: none; }
fieldset:has(input[value="0"]:checked) [data-out] [data-v="0"], fieldset:has(input[value="1"]:checked) [data-out] [data-v="1"],
fieldset:has(input[value="2"]:checked) [data-out] [data-v="2"], fieldset:has(input[value="3"]:checked) [data-out] [data-v="3"],
fieldset:has(input[value="4"]:checked) [data-out] [data-v="4"], fieldset:has(input[value="5"]:checked) [data-out] [data-v="5"],
fieldset:has(input[value="6"]:checked) [data-out] [data-v="6"], fieldset:has(input[value="7"]:checked) [data-out] [data-v="7"],
fieldset:has(input[value="8"]:checked) [data-out] [data-v="8"], fieldset:has(input[value="9"]:checked) [data-out] [data-v="9"],
fieldset:has(input[value="10"]:checked) [data-out] [data-v="10"] { display: inline; }

About this set

Rating scales for when stars are the wrong fit. Hearts rates a recipe from one to five hearts with a Clear button and a text label such as Loved it, using the same sibling technique as the star input. Thumbs up or down asks whether a help article was useful with two buttons, Yes and No; the chosen one turns solid, its thumb icon fills in, and a thank-you line appears. Faces with words is a satisfaction scale from Very unhappy to Very happy, with a drawn face and its word for each option; the chosen face takes its own colour, rises slightly and its word turns bold. Net Promoter Score is the standard 0 to 10 likelihood question with eleven numbered tiles, the ends of the scale written out, and the chosen tile coloured by band: red for 0 to 6, amber for 7 and 8, green for 9 and 10. Every scale is a native radio group in a fieldset, so each one works with Tab and the arrow keys and posts a value with a form, and none of them needs JavaScript.

What’s included

  • Heart rating with a Clear option
  • Yes or No feedback with filled icon on selection
  • Five faces with visible text labels
  • 0 to 10 NPS scale that wraps to two rows on phones
  • Native radio groups, no JavaScript

Accessibility

Each scale is a fieldset with a legend, and every option has a text name: 3 hearts, Yes, Neutral, or 0, not at all likely. Faces and NPS tiles show their words and numbers as visible text, the chosen option is marked by more than colour (a filled icon, a bar, bold text or a ring), and every option shows a 3px focus outline. Transitions and movement stop under prefers-reduced-motion.

Customise it

Change --os-ink, --os-edge and --os-focus on .os, the --on colours on .os-hearts, the --tone and --tint pair on each face, and --band on the NPS tiles. Bootstrap uses .btn-check with .btn and the same variables; in Tailwind edit the os colours in @theme and the peer-checked: colour classes.