Open demo

CSS code

HTML
<div><span class="lbl">Heart toggle</span><div class="stack">
<label class="mark"><input type="checkbox" checked><span class="heart" aria-hidden="true">❤️</span>Favourite this recipe</label>
<label class="mark"><input type="checkbox"><span class="heart" aria-hidden="true">❤️</span>Favourite this article</label></div></div>
<div><span class="lbl">Star rating (radios, reversed order)</span><div class="rate" role="radiogroup" aria-label="Rating">
<input type="radio" name="s" id="s5"><label for="s5"><span aria-hidden="true">★</span><span class="sr-only">5 stars</span></label>
<input type="radio" name="s" id="s4" checked><label for="s4"><span aria-hidden="true">★</span><span class="sr-only">4 stars</span></label>
<input type="radio" name="s" id="s3"><label for="s3"><span aria-hidden="true">★</span><span class="sr-only">3 stars</span></label>
<input type="radio" name="s" id="s2"><label for="s2"><span aria-hidden="true">★</span><span class="sr-only">2 stars</span></label>
<input type="radio" name="s" id="s1"><label for="s1"><span aria-hidden="true">★</span><span class="sr-only">1 star</span></label>
</div></div>
<div><span class="lbl">Thumbs</span><div class="pair" role="radiogroup" aria-label="Was this helpful?">
<label class="mark"><input type="radio" name="t" checked><span class="thumb" aria-hidden="true">👍</span>Helpful</label>
<label class="mark"><input type="radio" name="t"><span class="thumb" aria-hidden="true">👎</span>Not helpful</label></div></div>
CSS
.stack{display:flex;flex-direction:column;gap:13px;align-items:flex-start}
.mark{position:relative;display:inline-flex;align-items:center;gap:11px;cursor:pointer;font-size:.92rem}
.mark input,.rate input{position:absolute;opacity:0;width:0;height:0}
.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}

/* heart and thumbs: an emoji that is greyed out until its input is checked */
.heart,.thumb{font-size:1.6rem;line-height:1;border-radius:6px;transition:transform .18s,filter .18s;filter:grayscale(1) opacity(.6)}
.mark input:checked+.heart,.mark input:checked+.thumb{filter:none;transform:scale(1.12)}
.mark input:focus-visible+.heart,.mark input:focus-visible+.thumb{outline:2px solid #2563eb;outline-offset:3px}
.pair{display:flex;gap:20px}

/* star rating: radios in reverse order (5 to 1), laid out right-to-left with row-reverse,
   so "this star and every star after it in the DOM" is "this star and every star to its left" */
.rate{display:inline-flex;flex-direction:row-reverse;gap:4px}
.rate label{font-size:1.7rem;line-height:1;color:#848fa3;cursor:pointer;border-radius:4px;transition:color .15s}
.rate label:hover,.rate label:hover~label,
.rate input:checked~label{color:#d97706}
.rate input:focus-visible+label{outline:2px solid #2563eb;outline-offset:2px}

@media(prefers-reduced-motion:reduce){.heart,.thumb,.rate label{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Heart toggle</span><div class="vstack gap-3 align-items-start">
<div><input type="checkbox" class="btn-check" id="h1" checked><label class="choice-mark" for="h1"><span class="choice-mark-glyph" aria-hidden="true">❤️</span>Favourite this recipe</label></div>
<div><input type="checkbox" class="btn-check" id="h2"><label class="choice-mark" for="h2"><span class="choice-mark-glyph" aria-hidden="true">❤️</span>Favourite this article</label></div></div></div>
<div><span class="lbl">Star rating (radios, reversed order)</span><div class="choice-rate" role="radiogroup" aria-label="Rating">
<input type="radio" class="btn-check" name="s" id="s5"><label for="s5"><span aria-hidden="true">★</span><span class="visually-hidden">5 stars</span></label>
<input type="radio" class="btn-check" name="s" id="s4" checked><label for="s4"><span aria-hidden="true">★</span><span class="visually-hidden">4 stars</span></label>
<input type="radio" class="btn-check" name="s" id="s3"><label for="s3"><span aria-hidden="true">★</span><span class="visually-hidden">3 stars</span></label>
<input type="radio" class="btn-check" name="s" id="s2"><label for="s2"><span aria-hidden="true">★</span><span class="visually-hidden">2 stars</span></label>
<input type="radio" class="btn-check" name="s" id="s1"><label for="s1"><span aria-hidden="true">★</span><span class="visually-hidden">1 star</span></label>
</div></div>
<div><span class="lbl">Thumbs</span><div class="d-flex gap-3" role="radiogroup" aria-label="Was this helpful?">
<div><input type="radio" class="btn-check" name="t" id="t1" checked><label class="choice-mark" for="t1"><span class="choice-mark-glyph" aria-hidden="true">👍</span>Helpful</label></div>
<div><input type="radio" class="btn-check" name="t" id="t2"><label class="choice-mark" for="t2"><span class="choice-mark-glyph" aria-hidden="true">👎</span>Not helpful</label></div></div></div>
CSS
/* inputs use Bootstrap's visually hidden .btn-check; the label draws the mark */
.choice-mark{display:inline-flex;align-items:center;gap:11px;cursor:pointer;font-size:.92rem}
.choice-mark-glyph{font-size:1.6rem;line-height:1;border-radius:6px;transition:transform .18s,filter .18s;filter:grayscale(1) opacity(.6)}
.btn-check:checked+.choice-mark .choice-mark-glyph{filter:none;transform:scale(1.12)}
.btn-check:focus-visible+.choice-mark .choice-mark-glyph{outline:2px solid #2563eb;outline-offset:3px}

/* star rating: radios in reverse order (5 to 1) shown right-to-left with row-reverse */
.choice-rate{--choice-star:#848fa3;--choice-star-on:#d97706;display:inline-flex;flex-direction:row-reverse;gap:4px}
.choice-rate label{font-size:1.7rem;line-height:1;color:var(--choice-star);cursor:pointer;border-radius:4px;transition:color .15s}
.choice-rate label:hover,.choice-rate label:hover~label,
.choice-rate .btn-check:checked~label{color:var(--choice-star-on)}
.choice-rate .btn-check:focus-visible+label{outline:2px solid #2563eb;outline-offset:2px}

@media(prefers-reduced-motion:reduce){.choice-mark-glyph,.choice-rate label{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-slate-500 uppercase">Heart toggle</span><div class="flex flex-col items-start gap-[13px]">
<label class="relative inline-flex cursor-pointer items-center gap-[11px] text-[.92rem]"><input type="checkbox" class="peer sr-only" checked><span class="rounded-md text-[1.6rem] leading-none opacity-60 grayscale transition-[scale,filter,opacity] duration-200 peer-checked:scale-112 peer-checked:opacity-100 peer-checked:grayscale-0 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-blue-600 motion-reduce:transition-none" aria-hidden="true">❤️</span>Favourite this recipe</label>
<label class="relative inline-flex cursor-pointer items-center gap-[11px] text-[.92rem]"><input type="checkbox" class="peer sr-only"><span class="rounded-md text-[1.6rem] leading-none opacity-60 grayscale transition-[scale,filter,opacity] duration-200 peer-checked:scale-112 peer-checked:opacity-100 peer-checked:grayscale-0 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-blue-600 motion-reduce:transition-none" aria-hidden="true">❤️</span>Favourite this article</label></div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-slate-500 uppercase">Star rating (radios, reversed order)</span><div class="inline-flex flex-row-reverse gap-1" role="radiogroup" aria-label="Rating">
<input type="radio" name="s" id="s5" class="peer sr-only"><label for="s5" class="peer cursor-pointer rounded text-[1.7rem] leading-none text-[#848fa3] transition-[background-color,border-color,color] duration-150 hover:text-amber-600 peer-hover:text-amber-600 peer-checked:text-amber-600 [input:focus-visible+&]:outline-2 [input:focus-visible+&]:outline-offset-2 [input:focus-visible+&]:outline-blue-600 motion-reduce:transition-none"><span aria-hidden="true">★</span><span class="sr-only">5 stars</span></label>
<input type="radio" name="s" id="s4" class="peer sr-only" checked><label for="s4" class="peer cursor-pointer rounded text-[1.7rem] leading-none text-[#848fa3] transition-[background-color,border-color,color] duration-150 hover:text-amber-600 peer-hover:text-amber-600 peer-checked:text-amber-600 [input:focus-visible+&]:outline-2 [input:focus-visible+&]:outline-offset-2 [input:focus-visible+&]:outline-blue-600 motion-reduce:transition-none"><span aria-hidden="true">★</span><span class="sr-only">4 stars</span></label>
<input type="radio" name="s" id="s3" class="peer sr-only"><label for="s3" class="peer cursor-pointer rounded text-[1.7rem] leading-none text-[#848fa3] transition-[background-color,border-color,color] duration-150 hover:text-amber-600 peer-hover:text-amber-600 peer-checked:text-amber-600 [input:focus-visible+&]:outline-2 [input:focus-visible+&]:outline-offset-2 [input:focus-visible+&]:outline-blue-600 motion-reduce:transition-none"><span aria-hidden="true">★</span><span class="sr-only">3 stars</span></label>
<input type="radio" name="s" id="s2" class="peer sr-only"><label for="s2" class="peer cursor-pointer rounded text-[1.7rem] leading-none text-[#848fa3] transition-[background-color,border-color,color] duration-150 hover:text-amber-600 peer-hover:text-amber-600 peer-checked:text-amber-600 [input:focus-visible+&]:outline-2 [input:focus-visible+&]:outline-offset-2 [input:focus-visible+&]:outline-blue-600 motion-reduce:transition-none"><span aria-hidden="true">★</span><span class="sr-only">2 stars</span></label>
<input type="radio" name="s" id="s1" class="peer sr-only"><label for="s1" class="peer cursor-pointer rounded text-[1.7rem] leading-none text-[#848fa3] transition-[background-color,border-color,color] duration-150 hover:text-amber-600 peer-hover:text-amber-600 peer-checked:text-amber-600 [input:focus-visible+&]:outline-2 [input:focus-visible+&]:outline-offset-2 [input:focus-visible+&]:outline-blue-600 motion-reduce:transition-none"><span aria-hidden="true">★</span><span class="sr-only">1 star</span></label>
</div></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-slate-500 uppercase">Thumbs</span><div class="flex gap-5" role="radiogroup" aria-label="Was this helpful?">
<label class="relative inline-flex cursor-pointer items-center gap-[11px] text-[.92rem]"><input type="radio" name="t" class="peer sr-only" checked><span class="rounded-md text-[1.6rem] leading-none opacity-60 grayscale transition-[scale,filter,opacity] duration-200 peer-checked:scale-112 peer-checked:opacity-100 peer-checked:grayscale-0 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-blue-600 motion-reduce:transition-none" aria-hidden="true">👍</span>Helpful</label>
<label class="relative inline-flex cursor-pointer items-center gap-[11px] text-[.92rem]"><input type="radio" name="t" class="peer sr-only"><span class="rounded-md text-[1.6rem] leading-none opacity-60 grayscale transition-[scale,filter,opacity] duration-200 peer-checked:scale-112 peer-checked:opacity-100 peer-checked:grayscale-0 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-blue-600 motion-reduce:transition-none" aria-hidden="true">👎</span>Not helpful</label></div></div>

About this set

Choice controls that carry some character. A heart toggle is a checkbox whose emoji heart is greyed out until the item is saved, then shows in full colour at a slightly larger size. A five-star rating is built from five radio buttons, so exactly one rating is selected and it submits with a form like any radio group. A thumbs up/down pair is a two-option radio group for quick feedback on articles and help pages. The star rating uses the reversed-order technique: the radios run from five to one in the markup and the row is flipped with flex-direction row-reverse, so a sibling selector can colour the checked star and every star to its left, with a hover preview. The emoji marks use a CSS grayscale filter and scale transform keyed off the checked input. The Bootstrap version hides each input with .btn-check and uses .visually-hidden for the star names. The Tailwind version uses peer-checked, peer-hover and grayscale utilities.

What’s included

  • Heart toggle built on a checkbox
  • Five-star rating built from radios, with hover preview
  • Thumbs up/down radio pair
  • Stars and emoji hidden from screen readers, with text names
  • No JavaScript

Accessibility

Every control is a real input: the hearts and thumbs are labelled by their visible text, and each star has visually hidden text such as 4 stars while the glyph is aria-hidden. The rating and thumbs groups have role="radiogroup" with an aria-label. Focus draws a 2px blue outline around the heart, thumb or star. Because the stars are in reverse order, the arrow keys move from higher to lower ratings. Scale and colour transitions are disabled under prefers-reduced-motion.

Customise it

Plain CSS: change #d97706 (selected) and #848fa3 (empty) in the .rate rules and the font-size of .heart, .thumb and the stars. Bootstrap: set --choice-star and --choice-star-on on .choice-rate. Tailwind: swap amber-600 and text-[#848fa3] on the star labels.