Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><symbol id="mc-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">:has() input, no reversed markup</span>
<form class="mc" action="#">
  <fieldset>
    <legend>Rate your barista</legend>
    <p class="mc-sub">Order #2291 · flat white and a cardamom bun</p>
    <div class="mc-row">
      <div class="mc-stars">
        <label><input type="radio" name="barista" value="1"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">1 star</span></label>
        <label><input type="radio" name="barista" value="2"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">2 stars</span></label>
        <label><input type="radio" name="barista" value="3"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">3 stars</span></label>
        <label><input type="radio" name="barista" value="4"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">4 stars</span></label>
        <label><input type="radio" name="barista" value="5"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">5 stars</span></label>
      </div>
      <p class="mc-out" aria-hidden="true"><span data-v="0">Tap a star</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>
      <button class="mc-reset" type="reset">Clear<span class="sr-only"> rating</span></button>
    </div>
  </fieldset>
</form></div>

<div><span class="lbl">@property animated fill</span>
<div class="mc mc-anim">
  <article class="mc-tile" style="--to:4.6"><span class="mc-ring"><span><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></span></span><h3>Northline Coffee</h3><span class="mc-fill" aria-hidden="true"></span><p>1,932 ratings</p></article>
  <article class="mc-tile" style="--to:3.9"><span class="mc-ring"><span><span class="sr-only">Rated </span>3.9<span class="sr-only"> out of 5</span></span></span><h3>Kiln & Crumb Bakery</h3><span class="mc-fill" aria-hidden="true"></span><p>604 ratings</p></article>
  <article class="mc-tile" style="--to:4.2"><span class="mc-ring"><span><span class="sr-only">Rated </span>4.2<span class="sr-only"> out of 5</span></span></span><h3>Tram Stop Deli</h3><span class="mc-fill" aria-hidden="true"></span><p>287 ratings</p></article>
</div></div>

<div><span class="lbl">Container queries: one widget, three slots</span>
<div class="slots">
  <div class="slot"><small aria-hidden="true">200px slot</small>
    <div class="mc mc-cq"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul aria-label="Rating breakdown"><li><span>5 stars</span><i style="--w:74%" aria-hidden="true"></i><span>74%</span></li><li><span>4 stars</span><i style="--w:16%" aria-hidden="true"></i><span>16%</span></li><li><span>3 stars</span><i style="--w:6%" aria-hidden="true"></i><span>6%</span></li><li><span>2 stars</span><i style="--w:3%" aria-hidden="true"></i><span>3%</span></li><li><span>1 star</span><i style="--w:1%" aria-hidden="true"></i><span>1%</span></li></ul></div></div></div>
  <div class="slot m"><small aria-hidden="true">340px slot</small>
    <div class="mc mc-cq"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul aria-label="Rating breakdown"><li><span>5 stars</span><i style="--w:74%" aria-hidden="true"></i><span>74%</span></li><li><span>4 stars</span><i style="--w:16%" aria-hidden="true"></i><span>16%</span></li><li><span>3 stars</span><i style="--w:6%" aria-hidden="true"></i><span>6%</span></li><li><span>2 stars</span><i style="--w:3%" aria-hidden="true"></i><span>3%</span></li><li><span>1 star</span><i style="--w:1%" aria-hidden="true"></i><span>1%</span></li></ul></div></div></div>
  <div class="slot w"><small aria-hidden="true">Full width</small>
    <div class="mc mc-cq"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul aria-label="Rating breakdown"><li><span>5 stars</span><i style="--w:74%" aria-hidden="true"></i><span>74%</span></li><li><span>4 stars</span><i style="--w:16%" aria-hidden="true"></i><span>16%</span></li><li><span>3 stars</span><i style="--w:6%" aria-hidden="true"></i><span>6%</span></li><li><span>2 stars</span><i style="--w:3%" aria-hidden="true"></i><span>3%</span></li><li><span>1 star</span><i style="--w:1%" aria-hidden="true"></i><span>1%</span></li></ul></div></div></div>
</div></div>
CSS
/* Modern CSS star ratings.
   1. :has() — inputs sit inside their labels in natural 1→5 order. A star is filled when its own radio is checked or a
      LATER label holds the checked radio (label:has(~ label input:checked)); hover previews the same way. No reversed
      markup, no hidden "0" radio. A reset button clears the group.
   2. @property — --mc-r is a registered <number>, so the browser can animate it from 0 to the score; the star fill and
      the ring both read it. Unregistered, it would jump straight to the end value.
   3. Container queries — one rating widget that lays itself out by the width of its slot, not the window. */
.mc{--mc-on:#f5a524;--mc-on-edge:#a14b05;--mc-off:#efedf6;--mc-edge:#857f9e;--mc-ink:#1e1b2e;--mc-muted:#565170;--mc-line:#e3e0ee;
  --mc-accent:#6d28d9;--mc-focus:#6d28d9;
  --mc-star:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E");
  padding:26px 28px;background:#fff;border:1px solid var(--mc-line);border-radius:22px;color:var(--mc-ink)}
.mc .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 — :has() star input */
.mc fieldset{border:0;margin:0;padding:0;min-width:0}
.mc legend{padding:0;font-size:1.05rem;font-weight:700}
.mc-sub{margin-top:4px;font-size:.88rem;color:var(--mc-muted)}
.mc-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;margin-top:14px}
.mc-stars{display:flex}
.mc-stars label{position:relative;display:block;padding:4px;border-radius:10px;cursor:pointer}
.mc-stars input{position:absolute;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.mc-stars svg{display:block;width:36px;height:36px;fill:var(--mc-off);stroke:var(--mc-edge);stroke-width:1.1;stroke-linejoin:round;transition:fill .15s,stroke .15s,transform .15s}
/* filled: the checked star and every star before it */
.mc-stars label:has(input:checked) svg,.mc-stars label:has(~ label input:checked) svg{fill:var(--mc-on);stroke:var(--mc-on-edge)}
/* hover preview: empty everything, then fill the hovered star and those before it */
@media (hover:hover){
  .mc .mc-stars:has(label:hover) label svg{fill:var(--mc-off);stroke:var(--mc-edge)}
  .mc .mc-stars:has(label:hover) label:hover svg,.mc .mc-stars:has(label:hover) label:has(~ label:hover) svg{fill:var(--mc-on);stroke:var(--mc-on-edge)}
  .mc .mc-stars label:hover svg{transform:scale(1.12)}
}
.mc-stars label::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px}
.mc-stars label:has(input:checked)::after{background:var(--mc-ink)}
.mc-stars label:has(input:focus-visible){outline:3px solid var(--mc-focus);outline-offset:1px}
.mc-out{font-size:.92rem;color:var(--mc-muted)}
.mc-out b{color:var(--mc-ink)}
.mc-out span{display:none}
.mc-out [data-v="0"]{display:inline}
.mc:has(.mc-stars input:checked) .mc-out [data-v="0"]{display:none}
.mc:has(input[value="1"]:checked) .mc-out [data-v="1"],.mc:has(input[value="2"]:checked) .mc-out [data-v="2"],.mc:has(input[value="3"]:checked) .mc-out [data-v="3"],
.mc:has(input[value="4"]:checked) .mc-out [data-v="4"],.mc:has(input[value="5"]:checked) .mc-out [data-v="5"]{display:inline}
.mc-reset{margin-left:auto;padding:7px 14px;border:1px solid var(--mc-line);border-radius:999px;background:#fff;font:inherit;font-size:.82rem;font-weight:600;color:var(--mc-muted);cursor:pointer}
.mc-reset:hover{border-color:var(--mc-edge);color:var(--mc-ink)}
.mc-reset:focus-visible{outline:3px solid var(--mc-focus);outline-offset:2px}

/* 2 — @property animated fill */
@property --mc-r{syntax:"<number>";inherits:true;initial-value:0}
.mc-anim{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
.mc-tile{--mc-r:var(--to);display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:6px 16px;padding:18px 20px;
  border:1px solid var(--mc-line);border-radius:18px;background:linear-gradient(180deg,#fff,#faf9fe);animation:mc-fill 1.4s cubic-bezier(.2,.7,.2,1) .2s both}
@keyframes mc-fill{from{--mc-r:0}}
.mc-ring{grid-row:span 3;display:grid;place-items:center;width:68px;height:68px;border-radius:50%;
  background:radial-gradient(closest-side,#fff calc(100% - 7px),transparent calc(100% - 6px)),conic-gradient(var(--mc-accent) calc(var(--mc-r) * 72deg),#e6e2f2 0);
  font-size:1.35rem;font-weight:800;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.mc-tile h3{margin:0;font-size:.95rem;font-weight:700}
.mc-fill{--s:18px;display:block;width:calc(var(--s) * 5);height:var(--s);
  background:linear-gradient(90deg,var(--mc-on) calc(var(--mc-r) * 20%),#dcd8e8 0);
  -webkit-mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x;mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x}
.mc-tile p{margin:0;font-size:.85rem;color:var(--mc-muted)}

/* 3 — container queries */
.mc.mc-cq{container-type:inline-size;padding:0;border:0;background:none}
.mc-widget{display:flex;align-items:center;gap:8px;padding:12px 14px;border:1px solid var(--mc-line);border-radius:16px;background:#fff;font-variant-numeric:tabular-nums}
.mc-widget .mc-one{width:18px;height:18px;flex:none;background:var(--mc-on);-webkit-mask:var(--mc-star) center/contain no-repeat;mask:var(--mc-star) center/contain no-repeat}
.mc-widget .mc-five{display:none;--s:18px;flex:none;width:calc(var(--s) * 5);height:var(--s);background:linear-gradient(90deg,var(--mc-on) 92%,#dcd8e8 0);
  -webkit-mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x;mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x}
.mc-widget b{font-size:1.05rem}
.mc-widget small{font-size:.85rem;color:var(--mc-muted)}
.mc-widget .mc-word{display:none}
.mc-widget ul{display:none;list-style:none;margin:0;padding:0}
@container (min-width:300px){
  .mc-widget .mc-one{display:none}
  .mc-widget .mc-five,.mc-widget .mc-word{display:inline}
  .mc-widget .mc-five{display:block}
}
@container (min-width:560px){
  .mc-widget{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:"score bars";gap:6px 28px;padding:20px 24px}
  .mc-head{grid-area:score;display:flex;flex-direction:column;gap:6px}
  .mc-widget b{font-size:2.4rem;line-height:1;letter-spacing:-.03em}
  .mc-widget ul{grid-area:bars;display:grid;gap:6px}
  .mc-widget li{display:grid;grid-template-columns:3.4em minmax(0,1fr) 4.4ch;align-items:center;gap:10px;font-size:.84rem}
  .mc-widget li>span:last-child{text-align:right;color:var(--mc-muted)}
  .mc-widget li i{display:block;height:7px;border-radius:99px;background:linear-gradient(90deg,var(--mc-accent) var(--w),#ebe8f4 0)}
}
.mc-head{display:flex;align-items:center;gap:8px}
@media (max-width:480px){.mc{padding:22px 18px}.mc-stars svg{width:32px;height:32px}}
@media (prefers-reduced-motion:reduce){.mc-tile{animation:none}.mc-stars svg{transition:none}.mc .mc-stars label:hover svg{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="mc-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">:has() input, no reversed markup</span>
<form class="mc card" action="#">
  <fieldset>
    <legend>Rate your barista</legend>
    <p class="mc-sub">Order #2291 · flat white and a cardamom bun</p>
    <div class="mc-row">
      <div class="mc-stars">
        <label><input type="radio" name="barista" value="1"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="visually-hidden">1 star</span></label>
        <label><input type="radio" name="barista" value="2"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="visually-hidden">2 stars</span></label>
        <label><input type="radio" name="barista" value="3"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="visually-hidden">3 stars</span></label>
        <label><input type="radio" name="barista" value="4"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="visually-hidden">4 stars</span></label>
        <label><input type="radio" name="barista" value="5"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="visually-hidden">5 stars</span></label>
      </div>
      <p class="mc-out" aria-hidden="true"><span data-v="0">Tap a star</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>
      <button class="mc-reset btn" type="reset">Clear<span class="visually-hidden"> rating</span></button>
    </div>
  </fieldset>
</form></div>

<div><span class="lbl">@property animated fill</span>
<div class="mc mc-anim card"><div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 g-3">
  <div class="col">  <article class="mc-tile" style="--to:4.6"><span class="mc-ring"><span><span class="visually-hidden">Rated </span>4.6<span class="visually-hidden"> out of 5</span></span></span><h3>Northline Coffee</h3><span class="mc-fill" aria-hidden="true"></span><p>1,932 ratings</p></article></div>
  <div class="col">  <article class="mc-tile" style="--to:3.9"><span class="mc-ring"><span><span class="visually-hidden">Rated </span>3.9<span class="visually-hidden"> out of 5</span></span></span><h3>Kiln & Crumb Bakery</h3><span class="mc-fill" aria-hidden="true"></span><p>604 ratings</p></article></div>
  <div class="col">  <article class="mc-tile" style="--to:4.2"><span class="mc-ring"><span><span class="visually-hidden">Rated </span>4.2<span class="visually-hidden"> out of 5</span></span></span><h3>Tram Stop Deli</h3><span class="mc-fill" aria-hidden="true"></span><p>287 ratings</p></article></div>
</div></div></div>

<div><span class="lbl">Container queries: one widget, three slots</span>
<div class="slots">
  <div class="slot"><small aria-hidden="true">200px slot</small>
    <div class="mc mc-cq card"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="visually-hidden">Rated </span>4.6<span class="visually-hidden"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="visually-hidden"> reviews</span></small></div>
      <ul class="list-unstyled" aria-label="Rating breakdown"><li><span>5 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:74%"></span></span><span>74%</span></li><li><span>4 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:16%"></span></span><span>16%</span></li><li><span>3 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:6%"></span></span><span>6%</span></li><li><span>2 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:3%"></span></span><span>3%</span></li><li><span>1 star</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:1%"></span></span><span>1%</span></li></ul></div></div></div>
  <div class="slot m"><small aria-hidden="true">340px slot</small>
    <div class="mc mc-cq card"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="visually-hidden">Rated </span>4.6<span class="visually-hidden"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="visually-hidden"> reviews</span></small></div>
      <ul class="list-unstyled" aria-label="Rating breakdown"><li><span>5 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:74%"></span></span><span>74%</span></li><li><span>4 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:16%"></span></span><span>16%</span></li><li><span>3 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:6%"></span></span><span>6%</span></li><li><span>2 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:3%"></span></span><span>3%</span></li><li><span>1 star</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:1%"></span></span><span>1%</span></li></ul></div></div></div>
  <div class="slot w"><small aria-hidden="true">Full width</small>
    <div class="mc mc-cq card"><div class="mc-widget"><div class="mc-head"><span class="mc-one" aria-hidden="true"></span><span class="mc-five" aria-hidden="true"></span><b><span class="visually-hidden">Rated </span>4.6<span class="visually-hidden"> out of 5</span></b><small>812<span class="mc-word" aria-hidden="true"> reviews</span><span class="visually-hidden"> reviews</span></small></div>
      <ul class="list-unstyled" aria-label="Rating breakdown"><li><span>5 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:74%"></span></span><span>74%</span></li><li><span>4 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:16%"></span></span><span>16%</span></li><li><span>3 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:6%"></span></span><span>6%</span></li><li><span>2 stars</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:3%"></span></span><span>3%</span></li><li><span>1 star</span><span class="progress" aria-hidden="true"><span class="progress-bar" style="width:1%"></span></span><span>1%</span></li></ul></div></div></div>
</div></div>
CSS
/* Modern CSS star ratings on Bootstrap: .card, .btn, the grid and .progress, with the modern parts in this block.
   1. :has() — inputs sit inside their labels in natural 1→5 order. A star is filled when its own radio is checked or a
      LATER label holds the checked radio (label:has(~ label input:checked)); hover previews the same way. No reversed
      markup, no hidden "0" radio. A reset button clears the group.
   2. @property — --mc-r is a registered <number>, so the browser can animate it from 0 to the score; the star fill and
      the ring both read it. Unregistered, it would jump straight to the end value.
   3. Container queries — one rating widget that lays itself out by the width of its slot, not the window. */
.mc{--mc-on:#f5a524;--mc-on-edge:#a14b05;--mc-off:#efedf6;--mc-edge:#857f9e;--mc-ink:#1e1b2e;--mc-muted:#565170;--mc-line:#e3e0ee;
  --mc-accent:#6d28d9;--mc-focus:#6d28d9;
  --mc-star:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E");
  --bs-card-border-color:var(--mc-line);--bs-card-border-radius:22px;--bs-card-color:var(--mc-ink);display:block;padding:26px 28px;color:var(--mc-ink)}
.mc p,.mc ul{margin-bottom:0}

/* 1 — :has() star input */
.mc fieldset{border:0;margin:0;padding:0;min-width:0}
.mc legend{float:none;margin:0;line-height:1.3;padding:0;font-size:1.05rem;font-weight:700}
.mc-sub{margin-top:4px;font-size:.88rem;color:var(--mc-muted)}
.mc-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;margin-top:14px}
.mc-stars{display:flex}
.mc-stars label{position:relative;display:block;padding:4px;border-radius:10px;cursor:pointer}
.mc-stars input{position:absolute;top:0;left:0;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}
.mc-stars svg{display:block;width:36px;height:36px;fill:var(--mc-off);stroke:var(--mc-edge);stroke-width:1.1;stroke-linejoin:round;transition:fill .15s,stroke .15s,transform .15s}
/* filled: the checked star and every star before it */
.mc-stars label:has(input:checked) svg,.mc-stars label:has(~ label input:checked) svg{fill:var(--mc-on);stroke:var(--mc-on-edge)}
/* hover preview: empty everything, then fill the hovered star and those before it */
@media (hover:hover){
  .mc .mc-stars:has(label:hover) label svg{fill:var(--mc-off);stroke:var(--mc-edge)}
  .mc .mc-stars:has(label:hover) label:hover svg,.mc .mc-stars:has(label:hover) label:has(~ label:hover) svg{fill:var(--mc-on);stroke:var(--mc-on-edge)}
  .mc .mc-stars label:hover svg{transform:scale(1.12)}
}
.mc-stars label::after{content:"";position:absolute;left:50%;bottom:-4px;width:14px;height:3px;margin-left:-7px;border-radius:2px}
.mc-stars label:has(input:checked)::after{background:var(--mc-ink)}
.mc-stars label:has(input:focus-visible){outline:3px solid var(--mc-focus);outline-offset:1px}
.mc-out{font-size:.92rem;color:var(--mc-muted)}
.mc-out b{color:var(--mc-ink)}
.mc-out span{display:none}
.mc-out [data-v="0"]{display:inline}
.mc:has(.mc-stars input:checked) .mc-out [data-v="0"]{display:none}
.mc:has(input[value="1"]:checked) .mc-out [data-v="1"],.mc:has(input[value="2"]:checked) .mc-out [data-v="2"],.mc:has(input[value="3"]:checked) .mc-out [data-v="3"],
.mc:has(input[value="4"]:checked) .mc-out [data-v="4"],.mc:has(input[value="5"]:checked) .mc-out [data-v="5"]{display:inline}
.mc-reset{--bs-btn-padding-x:14px;--bs-btn-padding-y:7px;--bs-btn-font-size:.82rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-border-radius:999px;
  --bs-btn-color:var(--mc-muted);--bs-btn-bg:#fff;--bs-btn-border-color:var(--mc-line);--bs-btn-hover-color:var(--mc-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--mc-edge);
  --bs-btn-active-color:var(--mc-ink);--bs-btn-active-bg:#f4f2fb;--bs-btn-active-border-color:var(--mc-edge);margin-left:auto}
.mc-reset:focus-visible{outline:3px solid var(--mc-focus);outline-offset:2px;box-shadow:none}

/* 2 — @property animated fill */
@property --mc-r{syntax:"<number>";inherits:true;initial-value:0}

.mc-tile{--mc-r:var(--to);display:grid;height:100%;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:6px 16px;padding:18px 20px;
  border:1px solid var(--mc-line);border-radius:18px;background:linear-gradient(180deg,#fff,#faf9fe);animation:mc-fill 1.4s cubic-bezier(.2,.7,.2,1) .2s both}
@keyframes mc-fill{from{--mc-r:0}}
.mc-ring{grid-row:span 3;display:grid;place-items:center;width:68px;height:68px;border-radius:50%;
  background:radial-gradient(closest-side,#fff calc(100% - 7px),transparent calc(100% - 6px)),conic-gradient(var(--mc-accent) calc(var(--mc-r) * 72deg),#e6e2f2 0);
  font-size:1.35rem;font-weight:800;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.mc-tile h3{margin:0;font-size:.95rem;font-weight:700}
.mc-fill{--s:18px;display:block;width:calc(var(--s) * 5);height:var(--s);
  background:linear-gradient(90deg,var(--mc-on) calc(var(--mc-r) * 20%),#dcd8e8 0);
  -webkit-mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x;mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x}
.mc-tile p{margin:0;font-size:.85rem;color:var(--mc-muted)}

/* 3 — container queries */
.mc.mc-cq{container-type:inline-size;padding:0;border:0;background:none}
.mc-widget{display:flex;align-items:center;gap:8px;padding:12px 14px;border:1px solid var(--mc-line);border-radius:16px;background:#fff;font-variant-numeric:tabular-nums}
.mc-widget .mc-one{width:18px;height:18px;flex:none;background:var(--mc-on);-webkit-mask:var(--mc-star) center/contain no-repeat;mask:var(--mc-star) center/contain no-repeat}
.mc-widget .mc-five{display:none;--s:18px;flex:none;width:calc(var(--s) * 5);height:var(--s);background:linear-gradient(90deg,var(--mc-on) 92%,#dcd8e8 0);
  -webkit-mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x;mask:var(--mc-star) 0 0/var(--s) var(--s) repeat-x}
.mc-widget b{font-size:1.05rem}
.mc-widget small{font-size:.85rem;color:var(--mc-muted)}
.mc-widget .mc-word{display:none}
.mc-widget ul{display:none}
@container (min-width:300px){
  .mc-widget .mc-one{display:none}
  .mc-widget .mc-five,.mc-widget .mc-word{display:inline}
  .mc-widget .mc-five{display:block}
}
@container (min-width:560px){
  .mc-widget{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:"score bars";gap:6px 28px;padding:20px 24px}
  .mc-head{grid-area:score;display:flex;flex-direction:column;gap:6px}
  .mc-widget b{font-size:2.4rem;line-height:1;letter-spacing:-.03em}
  .mc-widget ul{grid-area:bars;display:grid;gap:6px}
  .mc-widget li{display:grid;grid-template-columns:3.4em minmax(0,1fr) 4.4ch;align-items:center;gap:10px;font-size:.84rem}
  .mc-widget li>span:last-child{text-align:right;color:var(--mc-muted)}
  .mc-widget .progress{--bs-progress-height:7px;--bs-progress-bg:#ebe8f4;--bs-progress-border-radius:99px;--bs-progress-bar-bg:var(--mc-accent)}
}
.mc-head{display:flex;align-items:center;gap:8px}
@media (max-width:480px){.mc{padding:22px 18px}.mc-stars svg{width:32px;height:32px}}
@media (prefers-reduced-motion:reduce){.mc-tile{animation:none}.mc-stars svg{transition:none}.mc .mc-stars label: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="mc-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-[#565170] uppercase">:has() input, no reversed markup</span>
<form class="group rounded-[22px] border border-mc-line bg-white px-[18px] py-[22px] min-[481px]:px-7 min-[481px]:py-[26px] text-mc-ink" action="#">
  <fieldset class="m-0 min-w-0 border-0 p-0">
    <legend class="p-0 text-[1.05rem] font-bold">Rate your barista</legend>
    <p class="mt-1 text-[.88rem] text-mc-muted">Order #2291 · flat white and a cardamom bun</p>
    <div class="mt-3.5 flex flex-wrap items-center gap-x-3.5 gap-y-1.5">
      <div class="has-rate [--si-size:32px] min-[481px]:[--si-size:36px]">
        <label><input type="radio" name="barista" value="1"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">1 star</span></label>
        <label><input type="radio" name="barista" value="2"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">2 stars</span></label>
        <label><input type="radio" name="barista" value="3"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">3 stars</span></label>
        <label><input type="radio" name="barista" value="4"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">4 stars</span></label>
        <label><input type="radio" name="barista" value="5"><svg aria-hidden="true"><use href="#mc-star"/></svg><span class="sr-only">5 stars</span></label>
      </div>
      <p class="text-[.92rem] text-mc-muted" aria-hidden="true"><span data-v="0" class="group-has-[.has-rate_input:checked]:hidden">Tap a star</span><span class="hidden group-has-[input[value='1']:checked]:inline"><b class="text-mc-ink">1 of 5</b> · Poor</span><span class="hidden group-has-[input[value='2']:checked]:inline"><b class="text-mc-ink">2 of 5</b> · Fair</span><span class="hidden group-has-[input[value='3']:checked]:inline"><b class="text-mc-ink">3 of 5</b> · Good</span><span class="hidden group-has-[input[value='4']:checked]:inline"><b class="text-mc-ink">4 of 5</b> · Very good</span><span class="hidden group-has-[input[value='5']:checked]:inline"><b class="text-mc-ink">5 of 5</b> · Excellent</span></p>
      <button class="ml-auto cursor-pointer rounded-full border border-mc-line bg-white px-3.5 py-[7px] text-[.82rem] font-semibold text-mc-muted hover:border-mc-edge hover:text-mc-ink focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-mc-accent" type="reset">Clear<span class="sr-only"> rating</span></button>
    </div>
  </fieldset>
</form></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#565170] uppercase">@property animated fill</span>
<div class="rounded-[22px] border border-mc-line bg-white px-[18px] py-[22px] min-[481px]:px-7 min-[481px]:py-[26px] text-mc-ink grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-4">
  <article class="grid h-full grid-cols-[auto_minmax(0,1fr)] items-center gap-x-4 gap-y-1.5 rounded-[18px] border border-mc-line bg-linear-to-b from-white to-[#faf9fe] px-5 py-[18px] [--mc-r:var(--to)] animate-[mc-fill_1.4s_cubic-bezier(.2,.7,.2,1)_.2s_both] motion-reduce:animate-none" style="--to:4.6"><span class="row-span-3 grid size-[68px] place-items-center rounded-full bg-[radial-gradient(closest-side,#fff_calc(100%-7px),transparent_calc(100%-6px)),conic-gradient(var(--color-mc-accent)_calc(var(--mc-r)*72deg),#e6e2f2_0)] text-[1.35rem] font-extrabold tracking-[-.02em] tabular-nums"><span><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></span></span><h3 class="text-[.95rem] font-bold">Northline Coffee</h3><span class="mc-stars" aria-hidden="true"></span><p class="text-[.85rem] text-mc-muted">1,932 ratings</p></article>
  <article class="grid h-full grid-cols-[auto_minmax(0,1fr)] items-center gap-x-4 gap-y-1.5 rounded-[18px] border border-mc-line bg-linear-to-b from-white to-[#faf9fe] px-5 py-[18px] [--mc-r:var(--to)] animate-[mc-fill_1.4s_cubic-bezier(.2,.7,.2,1)_.2s_both] motion-reduce:animate-none" style="--to:3.9"><span class="row-span-3 grid size-[68px] place-items-center rounded-full bg-[radial-gradient(closest-side,#fff_calc(100%-7px),transparent_calc(100%-6px)),conic-gradient(var(--color-mc-accent)_calc(var(--mc-r)*72deg),#e6e2f2_0)] text-[1.35rem] font-extrabold tracking-[-.02em] tabular-nums"><span><span class="sr-only">Rated </span>3.9<span class="sr-only"> out of 5</span></span></span><h3 class="text-[.95rem] font-bold">Kiln & Crumb Bakery</h3><span class="mc-stars" aria-hidden="true"></span><p class="text-[.85rem] text-mc-muted">604 ratings</p></article>
  <article class="grid h-full grid-cols-[auto_minmax(0,1fr)] items-center gap-x-4 gap-y-1.5 rounded-[18px] border border-mc-line bg-linear-to-b from-white to-[#faf9fe] px-5 py-[18px] [--mc-r:var(--to)] animate-[mc-fill_1.4s_cubic-bezier(.2,.7,.2,1)_.2s_both] motion-reduce:animate-none" style="--to:4.2"><span class="row-span-3 grid size-[68px] place-items-center rounded-full bg-[radial-gradient(closest-side,#fff_calc(100%-7px),transparent_calc(100%-6px)),conic-gradient(var(--color-mc-accent)_calc(var(--mc-r)*72deg),#e6e2f2_0)] text-[1.35rem] font-extrabold tracking-[-.02em] tabular-nums"><span><span class="sr-only">Rated </span>4.2<span class="sr-only"> out of 5</span></span></span><h3 class="text-[.95rem] font-bold">Tram Stop Deli</h3><span class="mc-stars" aria-hidden="true"></span><p class="text-[.85rem] text-mc-muted">287 ratings</p></article>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#565170] uppercase">Container queries: one widget, three slots</span>
<div class="flex flex-wrap items-start gap-4">
  <div class="w-[200px] max-w-full flex-none"><small class="mb-1.5 block text-[.75rem] text-[#565170]" aria-hidden="true">200px slot</small>
    <div class="@container"><div class="flex items-center gap-2 rounded-2xl border border-mc-line bg-white px-3.5 py-3 tabular-nums @min-[560px]:grid @min-[560px]:grid-cols-[auto_minmax(0,1fr)] @min-[560px]:gap-x-7 @min-[560px]:gap-y-1.5 @min-[560px]:px-6 @min-[560px]:py-5"><div class="flex items-center gap-2 @min-[560px]:flex-col @min-[560px]:items-stretch @min-[560px]:gap-1.5"><span class="mc-star-one size-[18px] flex-none @min-[300px]:hidden" aria-hidden="true"></span><span class="mc-stars hidden [--mc-r:4.6] @min-[300px]:block" aria-hidden="true"></span><b class="text-[1.05rem] @min-[560px]:text-[2.4rem] @min-[560px]:leading-none @min-[560px]:tracking-[-.03em]"><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small class="text-[.85rem] text-mc-muted">812<span class="hidden @min-[300px]:inline" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul class="hidden @min-[560px]:grid @min-[560px]:gap-1.5" aria-label="Rating breakdown"><li class="grid grid-cols-[3.4em_minmax(0,1fr)_4.4ch] items-center gap-2.5 text-[.84rem]"><span>5 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:74%"></span></span><span class="text-right text-mc-muted">74%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>4 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:16%"></span></span><span class="text-right text-mc-muted">16%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>3 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:6%"></span></span><span class="text-right text-mc-muted">6%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>2 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:3%"></span></span><span class="text-right text-mc-muted">3%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>1 star</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:1%"></span></span><span class="text-right text-mc-muted">1%</span></li></ul></div></div></div>
  <div class="w-[340px] max-w-full flex-none"><small class="mb-1.5 block text-[.75rem] text-[#565170]" aria-hidden="true">340px slot</small>
    <div class="@container"><div class="flex items-center gap-2 rounded-2xl border border-mc-line bg-white px-3.5 py-3 tabular-nums @min-[560px]:grid @min-[560px]:grid-cols-[auto_minmax(0,1fr)] @min-[560px]:gap-x-7 @min-[560px]:gap-y-1.5 @min-[560px]:px-6 @min-[560px]:py-5"><div class="flex items-center gap-2 @min-[560px]:flex-col @min-[560px]:items-stretch @min-[560px]:gap-1.5"><span class="mc-star-one size-[18px] flex-none @min-[300px]:hidden" aria-hidden="true"></span><span class="mc-stars hidden [--mc-r:4.6] @min-[300px]:block" aria-hidden="true"></span><b class="text-[1.05rem] @min-[560px]:text-[2.4rem] @min-[560px]:leading-none @min-[560px]:tracking-[-.03em]"><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small class="text-[.85rem] text-mc-muted">812<span class="hidden @min-[300px]:inline" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul class="hidden @min-[560px]:grid @min-[560px]:gap-1.5" aria-label="Rating breakdown"><li class="grid grid-cols-[3.4em_minmax(0,1fr)_4.4ch] items-center gap-2.5 text-[.84rem]"><span>5 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:74%"></span></span><span class="text-right text-mc-muted">74%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>4 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:16%"></span></span><span class="text-right text-mc-muted">16%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>3 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:6%"></span></span><span class="text-right text-mc-muted">6%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>2 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:3%"></span></span><span class="text-right text-mc-muted">3%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>1 star</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:1%"></span></span><span class="text-right text-mc-muted">1%</span></li></ul></div></div></div>
  <div class="w-auto flex-[1_1_100%]"><small class="mb-1.5 block text-[.75rem] text-[#565170]" aria-hidden="true">Full width</small>
    <div class="@container"><div class="flex items-center gap-2 rounded-2xl border border-mc-line bg-white px-3.5 py-3 tabular-nums @min-[560px]:grid @min-[560px]:grid-cols-[auto_minmax(0,1fr)] @min-[560px]:gap-x-7 @min-[560px]:gap-y-1.5 @min-[560px]:px-6 @min-[560px]:py-5"><div class="flex items-center gap-2 @min-[560px]:flex-col @min-[560px]:items-stretch @min-[560px]:gap-1.5"><span class="mc-star-one size-[18px] flex-none @min-[300px]:hidden" aria-hidden="true"></span><span class="mc-stars hidden [--mc-r:4.6] @min-[300px]:block" aria-hidden="true"></span><b class="text-[1.05rem] @min-[560px]:text-[2.4rem] @min-[560px]:leading-none @min-[560px]:tracking-[-.03em]"><span class="sr-only">Rated </span>4.6<span class="sr-only"> out of 5</span></b><small class="text-[.85rem] text-mc-muted">812<span class="hidden @min-[300px]:inline" aria-hidden="true"> reviews</span><span class="sr-only"> reviews</span></small></div>
      <ul class="hidden @min-[560px]:grid @min-[560px]:gap-1.5" aria-label="Rating breakdown"><li class="grid grid-cols-[3.4em_minmax(0,1fr)_4.4ch] items-center gap-2.5 text-[.84rem]"><span>5 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:74%"></span></span><span class="text-right text-mc-muted">74%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>4 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:16%"></span></span><span class="text-right text-mc-muted">16%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>3 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:6%"></span></span><span class="text-right text-mc-muted">6%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>2 stars</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:3%"></span></span><span class="text-right text-mc-muted">3%</span></li><li class="grid grid-cols-[3.4em_minmax(0,1fr)_3.4ch] items-center gap-2.5 text-[.84rem]"><span>1 star</span><span class="block h-[7px] rounded-full bg-[#ebe8f4]" aria-hidden="true"><span class="block h-full rounded-full bg-mc-accent" style="width:1%"></span></span><span class="text-right text-mc-muted">1%</span></li></ul></div></div></div>
</div></div>
CSS
@theme {
  --color-mc-on: #f5a524;
  --color-mc-on-edge: #a14b05;
  --color-mc-off: #efedf6;
  --color-mc-edge: #857f9e;
  --color-mc-ink: #1e1b2e;
  --color-mc-muted: #565170;
  --color-mc-line: #e3e0ee;
  --color-mc-accent: #6d28d9;
}
/* 1 — :has() star input: inputs inside their labels in natural 1→5 order. A star is filled when its radio is checked
   or a later label holds the checked radio; hover previews the same way. */
@utility has-rate {
  display: flex;
  & label { position: relative; display: block; padding: 4px; border-radius: 10px; cursor: pointer; }
  & input { position: absolute; top: 0; left: 0; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
  & svg { display: block; width: var(--si-size, 36px); height: var(--si-size, 36px); fill: var(--color-mc-off); stroke: var(--color-mc-edge); stroke-width: 1.1; stroke-linejoin: round; transition: fill .15s, stroke .15s, transform .15s; }
  & label:has(input:checked) svg, & label:has(~ label input:checked) svg { fill: var(--color-mc-on); stroke: var(--color-mc-on-edge); }
  @media (hover: hover) {
    &&:has(label:hover) label svg { fill: var(--color-mc-off); stroke: var(--color-mc-edge); }
    &&:has(label:hover) label:hover svg, &&:has(label:hover) label:has(~ label:hover) svg { fill: var(--color-mc-on); stroke: var(--color-mc-on-edge); }
    & label:hover svg { transform: scale(1.12); }
  }
  & label::after { content: ""; position: absolute; left: 50%; bottom: -4px; width: 14px; height: 3px; margin-left: -7px; border-radius: 2px; }
  & label:has(input:checked)::after { background: var(--color-mc-ink); }
  & label:has(input:focus-visible) { outline: 3px solid var(--color-mc-accent); outline-offset: 1px; }
  @media (prefers-reduced-motion: reduce) { & svg { transition: none; } & label:hover svg { transform: none; } }
}
/* 2 — @property: a registered <number> the browser can animate from 0 to the score; the ring and the stars read it. */
@property --mc-r { syntax: "<number>"; inherits: true; initial-value: 0; }
@keyframes mc-fill { from { --mc-r: 0; } }
@utility mc-stars {
  --s: 18px;
  display: block;
  width: calc(var(--s) * 5);
  height: var(--s);
  background: linear-gradient(90deg, var(--color-mc-on) calc(var(--mc-r, 5) * 20%), #dcd8e8 0);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E") 0 0 / var(--s) var(--s) repeat-x;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E") 0 0 / var(--s) var(--s) repeat-x;
}
@utility mc-star-one {
  background: var(--color-mc-on);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath 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'/%3E%3C/svg%3E") center / contain no-repeat;
}

About this set

Star ratings that use newer CSS for things that used to need tricks or JavaScript. :has() input, no reversed markup rates a barista with radios inside their labels in plain 1 to 5 order. A star is filled when its own radio is checked or when a later label holds the checked radio, and hover previews the same way, so there is no reversed markup and no hidden zero radio; a reset button clears the choice and the chosen value appears as text. @property animated fill shows three cafe scores where a registered number property animates from zero to the score on load, driving both the star fill and a ring around the number. Container queries places one rating widget in a 200 pixel slot, a 340 pixel slot and a full-width slot: the narrow one shows a single star and the count, the middle one five stars and the word reviews, and the wide one adds distribution bars, all decided by the width of the slot rather than the window.

What’s included

  • :has() star input in natural order with hover preview
  • Reset button clears the rating without JavaScript
  • @property animates the fill and a conic ring
  • Container queries switch the widget layout by slot width
  • Animation stops under prefers-reduced-motion

Accessibility

The input is a fieldset with a legend and radios named 1 star to 5 stars, with a 3px outline on the focused star and a bar under the chosen one. Animated scores are real text, such as Rated 4.6 out of 5, and only the graphics animate; under prefers-reduced-motion they show the final value at once. The container query widget keeps the rating text in every layout, and the reviews count is never read twice.

Customise it

Change --mc-on, --mc-off, --mc-edge, --mc-accent and --mc-focus on .mc. Set --to on each tile for its score, and adjust the 300px and 560px container breakpoints. In Tailwind edit the mc colours in @theme and the @min-[300px]: and @min-[560px]: classes.