Open demo

CSS code

HTML
<div><span class="lbl">Review list with filters, sorting and helpful votes</span>
<section class="rv" data-reviews aria-labelledby="rv-h">
  <div class="rv-sum">
    <h2 class="rv-h" id="rv-h">Reviews of the Kestrel 28 L daypack</h2>
    <p class="rv-avg">4.4</p>
    <div class="rv-avg-s"><span class="rv-stars" style="--r:4.4" role="img" aria-label="4.4 out of 5 stars">★★★★★</span><span>Based on 312 reviews</span></div>
    <p class="rv-rec"><b>91%</b> would recommend it to a friend</p>
  </div>
  <div class="rv-bar">
    <div class="rv-chips" role="group" aria-label="Filter reviews"><button class="rv-chip" type="button" aria-pressed="true" data-filter="all">All</button><button class="rv-chip" type="button" aria-pressed="false" data-filter="5">5 stars</button><button class="rv-chip" type="button" aria-pressed="false" data-filter="4">4 stars</button><button class="rv-chip" type="button" aria-pressed="false" data-filter="low">3 stars or lower</button><button class="rv-chip" type="button" aria-pressed="false" data-filter="verified">Verified only</button></div>
    <div class="rv-sort"><label class="rv-sort-l" for="rv-sort">Sort</label><select class="rv-select" id="rv-sort" data-sort><option value="helpful">Most helpful</option><option value="new">Newest</option><option value="low">Lowest rating</option></select></div>
  </div>
  <p class="rv-shown" aria-live="polite" data-shown>Showing 6 of 6 reviews</p>
  <ol class="rv-list" data-list>
    <li class="rv-item" data-rating="5" data-verified="true" data-date="2026-10-12" data-helpful="24">
      <article aria-labelledby="rv-t1">
        <div class="rv-top"><span class="rv-av c1" aria-hidden="true">AO</span><p class="rv-who"><b>Amara Okeke</b><span class="rv-m">Bristol, UK</span></p><span class="rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t1">Carries a laptop and a rain jacket without sagging</h3><time class="rv-date" datetime="2026-10-12">12 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>Comfortable all day</dd></div></dl>
        <p class="rv-text">I cycle 9 km each way and the back panel stays dry. The sleeve takes a 15-inch laptop with room left for the charger.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h1" data-helpful-group><span id="rv-h1">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="24"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>24</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="4" data-verified="true" data-date="2026-10-03" data-helpful="17">
      <article aria-labelledby="rv-t2">
        <div class="rv-top"><span class="rv-av c2" aria-hidden="true">HD</span><p class="rv-who"><b>Henrik Dahl</b><span class="rv-m">Bergen, Norway</span></p><span class="rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:4" role="img" aria-label="4 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t2">Great pack, but the hip belt is thin</h3><time class="rv-date" datetime="2026-10-03">3 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Hiking</dd></div><div><dt>Fit</dt><dd>Runs slightly long</dd></div></dl>
        <p class="rv-text">Light and well organised. On a full-day hike with 8 kg inside, the thin hip belt started to dig in after a few hours.</p>
        <div class="rv-seller"><b>Response from Kestrel Outdoors</b>Thanks, Henrik. A padded hip belt that clips onto the same loops goes on sale in November, and existing owners can swap theirs for free.</div>
        <div class="rv-help" role="group" aria-labelledby="rv-h2" data-helpful-group><span id="rv-h2">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="17"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>17</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="2"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>2</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="5" data-verified="false" data-date="2026-09-28" data-helpful="11">
      <article aria-labelledby="rv-t3">
        <div class="rv-top"><span class="rv-av c3" aria-hidden="true">ML</span><p class="rv-who"><b>Mei Lin</b><span class="rv-m">Singapore</span></p></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t3">Survived a week of monsoon rain</h3><time class="rv-date" datetime="2026-09-28">28 Sep 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">The roll-top and taped seams kept my camera dry through a week of downpours in Kerala.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h3" data-helpful-group><span id="rv-h3">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="11"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>11</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="3" data-verified="true" data-date="2026-09-20" data-helpful="9">
      <article aria-labelledby="rv-t4">
        <div class="rv-top"><span class="rv-av c4" aria-hidden="true">CM</span><p class="rv-who"><b>Carlos Mendes</b><span class="rv-m">Porto, Portugal</span></p><span class="rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:3" role="img" aria-label="3 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t4">Solid build, awkward side pockets</h3><time class="rv-date" datetime="2026-09-20">20 Sep 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Day hikes</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">A 1-litre bottle only fits in the side pocket when the main compartment is half empty.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h4" data-helpful-group><span id="rv-h4">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="9"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>9</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>3</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="5" data-verified="true" data-date="2026-10-16" data-helpful="4">
      <article aria-labelledby="rv-t5">
        <div class="rv-top"><span class="rv-av c5" aria-hidden="true">FZ</span><p class="rv-who"><b>Fatima Zahra</b><span class="rv-m">Rabat, Morocco</span></p><span class="rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t5">Fits under a budget airline seat</h3><time class="rv-date" datetime="2026-10-16">16 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">Packed for three days in Lisbon and it still slid under the seat in front of me.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h5" data-helpful-group><span id="rv-h5">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="4"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>4</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="2" data-verified="true" data-date="2026-10-08" data-helpful="6">
      <article aria-labelledby="rv-t6">
        <div class="rv-top"><span class="rv-av c6" aria-hidden="true">TB</span><p class="rv-who"><b>Tom Becker</b><span class="rv-m">Leipzig, Germany</span></p><span class="rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:2" role="img" aria-label="2 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t6">Zip pull snapped in month two</h3><time class="rv-date" datetime="2026-10-08">8 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">Support sent a replacement pull within a week, which I appreciate, but it should not have broken so soon.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h6" data-helpful-group><span id="rv-h6">Was this review helpful?</span>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="6"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>6</span></button>
          <button class="rv-hbtn" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
  </ol>
</section></div>
CSS
/* Review-style comments: a compact rating summary, filter chips and sort, review cards with verified badges, star ratings
   as labelled images, a seller response and "Was this helpful?" votes with aria-pressed. */
.rv{--rv-accent:#15803d;--rv-accent-dark:#166534;--rv-soft:#ecfdf3;--rv-ink:#17201a;--rv-muted:#526057;--rv-line:#e1e8e3;--rv-field:#7d8a82;--rv-star:#d97706;border:1px solid var(--rv-line);border-radius:18px;background:#fff;color:var(--rv-ink);font-size:.94rem;line-height:1.55;box-shadow:0 18px 40px -30px rgba(23,32,26,.4)}
.rv :where(ol){list-style:none}
.rv-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
:where(.rv) :is(button,select){font:inherit;color:inherit}
.rv :where(h2,h3,p,ol,dl,dd){margin:0}
.rv [hidden]{display:none!important}
.rv-sum{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px;padding:22px 24px;border-bottom:1px solid var(--rv-line)}
.rv-h{width:100%;font-size:1.15rem;font-weight:700;line-height:1.3}
.rv-avg{font-size:2.6rem;font-weight:800;line-height:1;letter-spacing:-.03em}
.rv-avg-s{display:flex;flex-direction:column;gap:4px;color:var(--rv-muted);font-size:.86rem}
/* stars: grey glyphs with a gold copy on top, clipped to the rating with --r */
.rv-stars{position:relative;display:inline-block;color:#c5cec8;font-size:1.15rem;line-height:1;letter-spacing:2px}
.rv-stars.sm{font-size:.95rem}
.rv-stars::before{content:"★★★★★";position:absolute;inset:0 auto 0 0;width:calc(var(--r) / 5 * 100%);overflow:hidden;color:var(--rv-star)}
.rv-rec{margin-left:auto;padding:10px 14px;border-radius:12px;background:var(--rv-soft);color:var(--rv-accent-dark);font-size:.86rem}
.rv-rec b{font-size:1.05rem}
.rv-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:16px 24px 0}
.rv-chips{display:flex;flex-wrap:wrap;gap:6px}
.rv-sort{display:flex;align-items:center;gap:8px;margin-left:auto;color:var(--rv-muted);font-size:.84rem;font-weight:600}
.rv-shown{padding:12px 24px 0;color:var(--rv-muted);font-size:.84rem}
.rv-list{display:grid;gap:14px;margin:0;padding:14px 24px 24px}
.rv-item{padding:20px;border:1px solid var(--rv-line);border-radius:14px}
.rv-top{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.rv-av{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;font-size:.74rem;font-weight:700}
.rv .c1{background:#fed7aa;color:#7c2d12}
.rv .c2{background:#bfdbfe;color:#1e3a8a}
.rv .c3{background:#fbcfe8;color:#831843}
.rv .c4{background:#e9d5ff;color:#581c87}
.rv .c5{background:#fde68a;color:#713f12}
.rv .c6{background:#cffafe;color:#164e63}
.rv-who{flex:1;min-width:0;font-size:.86rem;line-height:1.35}
.rv-who b{display:block;font-size:.92rem}
.rv-m,.rv-date{color:var(--rv-muted)}
.rv-date{font-size:.82rem}
.rv-ver{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;background:var(--rv-soft);color:var(--rv-accent-dark);font-size:.74rem;font-weight:700}
.rv-rate{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;margin-top:14px}
.rv-title{font-size:1rem;font-weight:700;line-height:1.35}
.rv-dl{display:flex;flex-wrap:wrap;gap:4px 16px;margin-top:8px;font-size:.82rem}
.rv-dl :is(dt,dd){display:inline;font-weight:400}
.rv-dl dt{color:var(--rv-muted)}
.rv-dl dt::after{content:": "}
.rv-text{margin-top:8px;text-wrap:pretty}
.rv-seller{margin-top:14px;padding:10px 14px;border-left:3px solid var(--rv-accent);border-radius:0 10px 10px 0;background:#f6f8f6;font-size:.88rem}
.rv-seller b{display:block;margin-bottom:2px;font-size:.8rem}
.rv-help{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;padding-top:14px;border-top:1px dashed var(--rv-line);color:var(--rv-muted);font-size:.84rem}
.rv-thanks{color:var(--rv-accent-dark);font-weight:600}
@media (max-width:640px){.rv-sum,.rv-bar,.rv-shown{padding-left:16px;padding-right:16px}.rv-list{padding:14px 16px 16px}.rv-item{padding:16px}.rv-rec{margin-left:0}}
.rv-chip{display:inline-flex;align-items:center;min-height:34px;padding:0 14px;border:1.5px solid var(--rv-line);border-radius:999px;background:#fff;color:var(--rv-ink);font-size:.84rem;font-weight:600;cursor:pointer;transition:background-color .15s,border-color .15s,color .15s}
.rv-chip:hover{border-color:var(--rv-field)}
.rv-chip[aria-pressed=true]{border-color:var(--rv-ink);background:var(--rv-ink);color:#fff}
.rv-select{min-height:34px;padding:4px 32px 4px 14px;border:1.5px solid var(--rv-field);border-radius:999px;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%2317201a' stroke-width='2' d='m3 6 5 5 5-5'/%3E%3C/svg%3E") no-repeat right 12px center/12px;color:var(--rv-ink);font-weight:600;cursor:pointer;appearance:none}
.rv-select:focus{border-color:var(--rv-accent);outline:2px solid transparent;box-shadow:0 0 0 3px rgba(21,128,61,.22)}
.rv-hbtn{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;border:1.5px solid var(--rv-field);border-radius:999px;background:#fff;color:var(--rv-ink);font-size:.82rem;font-weight:600;font-variant-numeric:tabular-nums;cursor:pointer;transition:background-color .15s,border-color .15s}
.rv-hbtn:hover{background:var(--rv-soft)}
.rv-hbtn[aria-pressed=true]{border-color:var(--rv-accent);background:var(--rv-soft);color:var(--rv-accent-dark)}
.rv :is(.rv-chip,.rv-hbtn):focus-visible{outline:2px solid var(--rv-accent);outline-offset:2px}
@media (max-width:640px){.rv-sort{margin-left:0}}
@media (prefers-reduced-motion:reduce){.rv *{transition:none}}
JavaScript
/* Review list: filter chips (one pressed at a time), sorting, and helpful votes (Yes or No, click again to undo).
   The "Showing n of n" line and the thanks note are polite live regions. Frontend only: post votes to your backend where marked. */
document.querySelectorAll('[data-reviews]').forEach(function (root) {
  var list = root.querySelector('[data-list]'), items = Array.from(list.children), shown = root.querySelector('[data-shown]'), sort = root.querySelector('[data-sort]');
  function filter(by) {
    var n = 0;
    items.forEach(function (li) {
      var r = +li.dataset.rating;
      var ok = by === 'all' || (by === 'verified' ? li.dataset.verified === 'true' : by === 'low' ? r <= 3 : r === +by);
      li.hidden = !ok; if (ok) n++;
    });
    shown.textContent = 'Showing ' + n + ' of ' + items.length + ' reviews';
  }
  root.querySelectorAll('[data-filter]').forEach(function (b) {
    b.addEventListener('click', function () {
      root.querySelectorAll('[data-filter]').forEach(function (x) { x.setAttribute('aria-pressed', x === b); });
      filter(b.dataset.filter);
    });
  });
  sort.addEventListener('change', function () {
    var by = sort.value;
    items.sort(function (a, b) {
      if (by === 'new') return b.dataset.date.localeCompare(a.dataset.date);
      if (by === 'low') return a.dataset.rating - b.dataset.rating;
      return b.dataset.helpful - a.dataset.helpful;
    });
    items.forEach(function (li) { list.appendChild(li); });
  });
  root.querySelectorAll('[data-helpful-group]').forEach(function (g) {
    var thanks = g.querySelector('[data-thanks]');
    g.addEventListener('click', function (e) {
      var b = e.target.closest('button');
      if (!b) return;
      var on = b.getAttribute('aria-pressed') !== 'true';
      g.querySelectorAll('button').forEach(function (x) {
        var p = x === b && on;
        x.setAttribute('aria-pressed', p);
        x.querySelector('[data-n]').textContent = +x.dataset.base + (p ? 1 : 0);
      });
      thanks.textContent = on ? 'Thanks for your feedback.' : ''; // send the vote here
    });
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Review list with filters, sorting and helpful votes</span>
<section class="card rv" data-reviews aria-labelledby="rv-h">
  <div class="rv-sum">
    <h2 class="rv-h" id="rv-h">Reviews of the Kestrel 28 L daypack</h2>
    <p class="rv-avg">4.4</p>
    <div class="rv-avg-s"><span class="rv-stars" style="--r:4.4" role="img" aria-label="4.4 out of 5 stars">★★★★★</span><span>Based on 312 reviews</span></div>
    <p class="rv-rec"><b>91%</b> would recommend it to a friend</p>
  </div>
  <div class="rv-bar">
    <div class="rv-chips" role="group" aria-label="Filter reviews"><button class="btn rv-chip" type="button" aria-pressed="true" data-filter="all">All</button><button class="btn rv-chip" type="button" aria-pressed="false" data-filter="5">5 stars</button><button class="btn rv-chip" type="button" aria-pressed="false" data-filter="4">4 stars</button><button class="btn rv-chip" type="button" aria-pressed="false" data-filter="low">3 stars or lower</button><button class="btn rv-chip" type="button" aria-pressed="false" data-filter="verified">Verified only</button></div>
    <div class="rv-sort"><label class="form-label rv-sort-l" for="rv-sort">Sort</label><select class="form-select rv-select" id="rv-sort" data-sort><option value="helpful">Most helpful</option><option value="new">Newest</option><option value="low">Lowest rating</option></select></div>
  </div>
  <p class="rv-shown" aria-live="polite" data-shown>Showing 6 of 6 reviews</p>
  <ol class="list-unstyled rv-list" data-list>
    <li class="rv-item" data-rating="5" data-verified="true" data-date="2026-10-12" data-helpful="24">
      <article aria-labelledby="rv-t1">
        <div class="rv-top"><span class="rv-av c1" aria-hidden="true">AO</span><p class="rv-who"><b>Amara Okeke</b><span class="rv-m">Bristol, UK</span></p><span class="badge rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t1">Carries a laptop and a rain jacket without sagging</h3><time class="rv-date" datetime="2026-10-12">12 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>Comfortable all day</dd></div></dl>
        <p class="rv-text">I cycle 9 km each way and the back panel stays dry. The sleeve takes a 15-inch laptop with room left for the charger.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h1" data-helpful-group><span id="rv-h1">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="24"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>24</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="4" data-verified="true" data-date="2026-10-03" data-helpful="17">
      <article aria-labelledby="rv-t2">
        <div class="rv-top"><span class="rv-av c2" aria-hidden="true">HD</span><p class="rv-who"><b>Henrik Dahl</b><span class="rv-m">Bergen, Norway</span></p><span class="badge rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:4" role="img" aria-label="4 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t2">Great pack, but the hip belt is thin</h3><time class="rv-date" datetime="2026-10-03">3 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Hiking</dd></div><div><dt>Fit</dt><dd>Runs slightly long</dd></div></dl>
        <p class="rv-text">Light and well organised. On a full-day hike with 8 kg inside, the thin hip belt started to dig in after a few hours.</p>
        <div class="rv-seller"><b>Response from Kestrel Outdoors</b>Thanks, Henrik. A padded hip belt that clips onto the same loops goes on sale in November, and existing owners can swap theirs for free.</div>
        <div class="rv-help" role="group" aria-labelledby="rv-h2" data-helpful-group><span id="rv-h2">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="17"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>17</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="2"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>2</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="5" data-verified="false" data-date="2026-09-28" data-helpful="11">
      <article aria-labelledby="rv-t3">
        <div class="rv-top"><span class="rv-av c3" aria-hidden="true">ML</span><p class="rv-who"><b>Mei Lin</b><span class="rv-m">Singapore</span></p></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t3">Survived a week of monsoon rain</h3><time class="rv-date" datetime="2026-09-28">28 Sep 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">The roll-top and taped seams kept my camera dry through a week of downpours in Kerala.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h3" data-helpful-group><span id="rv-h3">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="11"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>11</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="3" data-verified="true" data-date="2026-09-20" data-helpful="9">
      <article aria-labelledby="rv-t4">
        <div class="rv-top"><span class="rv-av c4" aria-hidden="true">CM</span><p class="rv-who"><b>Carlos Mendes</b><span class="rv-m">Porto, Portugal</span></p><span class="badge rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:3" role="img" aria-label="3 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t4">Solid build, awkward side pockets</h3><time class="rv-date" datetime="2026-09-20">20 Sep 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Day hikes</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">A 1-litre bottle only fits in the side pocket when the main compartment is half empty.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h4" data-helpful-group><span id="rv-h4">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="9"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>9</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>3</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="5" data-verified="true" data-date="2026-10-16" data-helpful="4">
      <article aria-labelledby="rv-t5">
        <div class="rv-top"><span class="rv-av c5" aria-hidden="true">FZ</span><p class="rv-who"><b>Fatima Zahra</b><span class="rv-m">Rabat, Morocco</span></p><span class="badge rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t5">Fits under a budget airline seat</h3><time class="rv-date" datetime="2026-10-16">16 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">Packed for three days in Lisbon and it still slid under the seat in front of me.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h5" data-helpful-group><span id="rv-h5">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="4"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>4</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rv-item" data-rating="2" data-verified="true" data-date="2026-10-08" data-helpful="6">
      <article aria-labelledby="rv-t6">
        <div class="rv-top"><span class="rv-av c6" aria-hidden="true">TB</span><p class="rv-who"><b>Tom Becker</b><span class="rv-m">Leipzig, Germany</span></p><span class="badge rv-ver"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="rv-rate"><span class="rv-stars sm" style="--r:2" role="img" aria-label="2 out of 5 stars">★★★★★</span><h3 class="rv-title" id="rv-t6">Zip pull snapped in month two</h3><time class="rv-date" datetime="2026-10-08">8 Oct 2026</time></div>
        <dl class="rv-dl"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="rv-text">Support sent a replacement pull within a week, which I appreciate, but it should not have broken so soon.</p>
        <div class="rv-help" role="group" aria-labelledby="rv-h6" data-helpful-group><span id="rv-h6">Was this review helpful?</span>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="6"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>6</span></button>
          <button class="btn rv-hbtn" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="rv-thanks" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
  </ol>
</section></div>
CSS
/* Review-style comments on Bootstrap: a .card with .btn filter chips and helpful votes, a .form-select sort and .badge labels.
   Star ratings are labelled images; helpful votes are aria-pressed toggle buttons. */
.rv{--rv-accent:#15803d;--rv-accent-dark:#166534;--rv-soft:#ecfdf3;--rv-ink:#17201a;--rv-muted:#526057;--rv-line:#e1e8e3;--rv-field:#7d8a82;--rv-star:#d97706;--bs-card-border-color:var(--rv-line);--bs-card-border-radius:18px;--bs-card-color:var(--rv-ink);font-size:.94rem;line-height:1.55;box-shadow:0 18px 40px -30px rgba(23,32,26,.4)}
.rv :where(h2,h3,p,ol,dl,dd){margin:0}
.rv [hidden]{display:none!important}
.rv-sum{display:flex;flex-wrap:wrap;align-items:center;gap:14px 24px;padding:22px 24px;border-bottom:1px solid var(--rv-line)}
.rv-h{width:100%;font-size:1.15rem;font-weight:700;line-height:1.3}
.rv-avg{font-size:2.6rem;font-weight:800;line-height:1;letter-spacing:-.03em}
.rv-avg-s{display:flex;flex-direction:column;gap:4px;color:var(--rv-muted);font-size:.86rem}
/* stars: grey glyphs with a gold copy on top, clipped to the rating with --r */
.rv-stars{position:relative;display:inline-block;color:#c5cec8;font-size:1.15rem;line-height:1;letter-spacing:2px}
.rv-stars.sm{font-size:.95rem}
.rv-stars::before{content:"★★★★★";position:absolute;inset:0 auto 0 0;width:calc(var(--r) / 5 * 100%);overflow:hidden;color:var(--rv-star)}
.rv-rec{margin-left:auto;padding:10px 14px;border-radius:12px;background:var(--rv-soft);color:var(--rv-accent-dark);font-size:.86rem}
.rv-rec b{font-size:1.05rem}
.rv-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:16px 24px 0}
.rv-chips{display:flex;flex-wrap:wrap;gap:6px}
.rv-sort{display:flex;align-items:center;gap:8px;margin-left:auto;color:var(--rv-muted);font-size:.84rem;font-weight:600}
.rv-shown{padding:12px 24px 0;color:var(--rv-muted);font-size:.84rem}
.rv-list{display:grid;gap:14px;margin:0;padding:14px 24px 24px}
.rv-item{padding:20px;border:1px solid var(--rv-line);border-radius:14px}
.rv-top{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.rv-av{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;font-size:.74rem;font-weight:700}
.rv .c1{background:#fed7aa;color:#7c2d12}
.rv .c2{background:#bfdbfe;color:#1e3a8a}
.rv .c3{background:#fbcfe8;color:#831843}
.rv .c4{background:#e9d5ff;color:#581c87}
.rv .c5{background:#fde68a;color:#713f12}
.rv .c6{background:#cffafe;color:#164e63}
.rv-who{flex:1;min-width:0;font-size:.86rem;line-height:1.35}
.rv-who b{display:block;font-size:.92rem}
.rv-m,.rv-date{color:var(--rv-muted)}
.rv-date{font-size:.82rem}
.rv-ver{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;background:var(--rv-soft);color:var(--rv-accent-dark);font-size:.74rem;font-weight:700}
.rv-rate{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;margin-top:14px}
.rv-title{font-size:1rem;font-weight:700;line-height:1.35}
.rv-dl{display:flex;flex-wrap:wrap;gap:4px 16px;margin-top:8px;font-size:.82rem}
.rv-dl :is(dt,dd){display:inline;font-weight:400}
.rv-dl dt{color:var(--rv-muted)}
.rv-dl dt::after{content:": "}
.rv-text{margin-top:8px;text-wrap:pretty}
.rv-seller{margin-top:14px;padding:10px 14px;border-left:3px solid var(--rv-accent);border-radius:0 10px 10px 0;background:#f6f8f6;font-size:.88rem}
.rv-seller b{display:block;margin-bottom:2px;font-size:.8rem}
.rv-help{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;padding-top:14px;border-top:1px dashed var(--rv-line);color:var(--rv-muted);font-size:.84rem}
.rv-thanks{color:var(--rv-accent-dark);font-weight:600}
@media (max-width:640px){.rv-sum,.rv-bar,.rv-shown{padding-left:16px;padding-right:16px}.rv-list{padding:14px 16px 16px}.rv-item{padding:16px}.rv-rec{margin-left:0}}
.rv .form-label{margin:0}
.rv .btn{--bs-btn-focus-box-shadow:none;--bs-btn-padding-y:0;--bs-btn-font-weight:600;--bs-btn-border-width:1.5px;--bs-btn-border-radius:999px}
.rv .btn:focus-visible{outline:2px solid var(--rv-accent);outline-offset:2px}
.rv-chip{--bs-btn-padding-x:14px;--bs-btn-font-size:.84rem;--bs-btn-color:var(--rv-ink);--bs-btn-bg:#fff;--bs-btn-border-color:var(--rv-line);--bs-btn-hover-color:var(--rv-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--rv-field);--bs-btn-active-color:var(--rv-ink);--bs-btn-active-bg:#fff;--bs-btn-active-border-color:var(--rv-field);display:inline-flex;align-items:center;min-height:34px}
.rv-chip[aria-pressed=true]{--bs-btn-color:#fff;--bs-btn-bg:var(--rv-ink);--bs-btn-border-color:var(--rv-ink);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--rv-ink);--bs-btn-hover-border-color:var(--rv-ink)}
.rv-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%2317201a' stroke-width='2' d='m3 6 5 5 5-5'/%3E%3C/svg%3E");width:auto;min-height:34px;padding:4px 32px 4px 14px;border:1.5px solid var(--rv-field);border-radius:999px;background-position:right 12px center;background-size:12px;color:var(--rv-ink);font-size:inherit;font-weight:600;cursor:pointer}
.rv-select:focus{border-color:var(--rv-accent);box-shadow:0 0 0 3px rgba(21,128,61,.22)}
.rv-ver{--bs-badge-padding-x:8px;--bs-badge-padding-y:3px;--bs-badge-font-size:.74rem;--bs-badge-color:var(--rv-accent-dark);--bs-badge-border-radius:999px}
.rv-hbtn{--bs-btn-padding-x:12px;--bs-btn-font-size:.82rem;--bs-btn-color:var(--rv-ink);--bs-btn-bg:#fff;--bs-btn-border-color:var(--rv-field);--bs-btn-hover-color:var(--rv-ink);--bs-btn-hover-bg:var(--rv-soft);--bs-btn-hover-border-color:var(--rv-field);--bs-btn-active-color:var(--rv-accent-dark);--bs-btn-active-bg:var(--rv-soft);--bs-btn-active-border-color:var(--rv-accent);display:inline-flex;align-items:center;gap:6px;min-height:32px;font-variant-numeric:tabular-nums}
.rv-hbtn[aria-pressed=true]{--bs-btn-color:var(--rv-accent-dark);--bs-btn-bg:var(--rv-soft);--bs-btn-border-color:var(--rv-accent);--bs-btn-hover-color:var(--rv-accent-dark);--bs-btn-hover-border-color:var(--rv-accent)}
@media (max-width:640px){.rv-sort{margin-left:0}}
@media (prefers-reduced-motion:reduce){.rv .btn{transition:none}}
JavaScript
/* Review list: filter chips (one pressed at a time), sorting, and helpful votes (Yes or No, click again to undo).
   The "Showing n of n" line and the thanks note are polite live regions. Frontend only: post votes to your backend where marked. */
document.querySelectorAll('[data-reviews]').forEach(function (root) {
  var list = root.querySelector('[data-list]'), items = Array.from(list.children), shown = root.querySelector('[data-shown]'), sort = root.querySelector('[data-sort]');
  function filter(by) {
    var n = 0;
    items.forEach(function (li) {
      var r = +li.dataset.rating;
      var ok = by === 'all' || (by === 'verified' ? li.dataset.verified === 'true' : by === 'low' ? r <= 3 : r === +by);
      li.hidden = !ok; if (ok) n++;
    });
    shown.textContent = 'Showing ' + n + ' of ' + items.length + ' reviews';
  }
  root.querySelectorAll('[data-filter]').forEach(function (b) {
    b.addEventListener('click', function () {
      root.querySelectorAll('[data-filter]').forEach(function (x) { x.setAttribute('aria-pressed', x === b); });
      filter(b.dataset.filter);
    });
  });
  sort.addEventListener('change', function () {
    var by = sort.value;
    items.sort(function (a, b) {
      if (by === 'new') return b.dataset.date.localeCompare(a.dataset.date);
      if (by === 'low') return a.dataset.rating - b.dataset.rating;
      return b.dataset.helpful - a.dataset.helpful;
    });
    items.forEach(function (li) { list.appendChild(li); });
  });
  root.querySelectorAll('[data-helpful-group]').forEach(function (g) {
    var thanks = g.querySelector('[data-thanks]');
    g.addEventListener('click', function (e) {
      var b = e.target.closest('button');
      if (!b) return;
      var on = b.getAttribute('aria-pressed') !== 'true';
      g.querySelectorAll('button').forEach(function (x) {
        var p = x === b && on;
        x.setAttribute('aria-pressed', p);
        x.querySelector('[data-n]').textContent = +x.dataset.base + (p ? 1 : 0);
      });
      thanks.textContent = on ? 'Thanks for your feedback.' : ''; // send the vote here
    });
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#526057] uppercase">Review list with filters, sorting and helpful votes</span>
<section class="rounded-[18px] border border-rv-line bg-white text-[.94rem] leading-[1.55] text-rv-ink shadow-[0_18px_40px_-30px_rgba(23,32,26,.4)]" data-reviews aria-labelledby="rv-h">
  <div class="flex flex-wrap items-center gap-x-6 gap-y-3.5 border-b border-rv-line px-6 py-[22px] max-sm:px-4">
    <h2 class="w-full text-[1.15rem] leading-[1.3] font-bold" id="rv-h">Reviews of the Kestrel 28 L daypack</h2>
    <p class="text-[2.6rem] leading-none font-extrabold tracking-[-.03em]">4.4</p>
    <div class="flex flex-col gap-1 text-[.86rem] text-rv-muted"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[1.15rem]" style="--r:4.4" role="img" aria-label="4.4 out of 5 stars">★★★★★</span><span>Based on 312 reviews</span></div>
    <p class="ml-auto rounded-xl bg-rv-soft px-3.5 py-2.5 text-[.86rem] text-rv-accent-dark max-sm:ml-0 [&_b]:text-[1.05rem]"><b>91%</b> would recommend it to a friend</p>
  </div>
  <div class="flex flex-wrap items-center gap-2 px-6 pt-4 max-sm:px-4">
    <div class="flex flex-wrap gap-1.5" role="group" aria-label="Filter reviews"><button class="inline-flex min-h-[34px] cursor-pointer items-center rounded-full border-[1.5px] border-rv-line bg-white px-3.5 text-[.84rem] font-semibold text-rv-ink transition-colors duration-150 hover:border-[#7d8a82] aria-pressed:border-rv-ink aria-pressed:bg-rv-ink aria-pressed:text-white motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="true" data-filter="all">All</button><button class="inline-flex min-h-[34px] cursor-pointer items-center rounded-full border-[1.5px] border-rv-line bg-white px-3.5 text-[.84rem] font-semibold text-rv-ink transition-colors duration-150 hover:border-[#7d8a82] aria-pressed:border-rv-ink aria-pressed:bg-rv-ink aria-pressed:text-white motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-filter="5">5 stars</button><button class="inline-flex min-h-[34px] cursor-pointer items-center rounded-full border-[1.5px] border-rv-line bg-white px-3.5 text-[.84rem] font-semibold text-rv-ink transition-colors duration-150 hover:border-[#7d8a82] aria-pressed:border-rv-ink aria-pressed:bg-rv-ink aria-pressed:text-white motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-filter="4">4 stars</button><button class="inline-flex min-h-[34px] cursor-pointer items-center rounded-full border-[1.5px] border-rv-line bg-white px-3.5 text-[.84rem] font-semibold text-rv-ink transition-colors duration-150 hover:border-[#7d8a82] aria-pressed:border-rv-ink aria-pressed:bg-rv-ink aria-pressed:text-white motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-filter="low">3 stars or lower</button><button class="inline-flex min-h-[34px] cursor-pointer items-center rounded-full border-[1.5px] border-rv-line bg-white px-3.5 text-[.84rem] font-semibold text-rv-ink transition-colors duration-150 hover:border-[#7d8a82] aria-pressed:border-rv-ink aria-pressed:bg-rv-ink aria-pressed:text-white motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-filter="verified">Verified only</button></div>
    <div class="ml-auto flex items-center gap-2 text-[.84rem] font-semibold text-rv-muted"><label class="" for="rv-sort">Sort</label><select class="min-h-[34px] cursor-pointer appearance-none rounded-full border-[1.5px] border-[#7d8a82] bg-white bg-[url('data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2016%2016%22%3E%3Cpath%20fill=%22none%22%20stroke=%22%2317201a%22%20stroke-width=%222%22%20d=%22m3%206%205%205%205-5%22/%3E%3C/svg%3E')] bg-[length:12px] bg-[right_12px_center] bg-no-repeat py-1 pr-8 pl-3.5 font-semibold text-rv-ink focus:border-rv-accent focus:shadow-[0_0_0_3px_rgba(21,128,61,.22)] focus:outline-2 focus:outline-transparent" id="rv-sort" data-sort><option value="helpful">Most helpful</option><option value="new">Newest</option><option value="low">Lowest rating</option></select></div>
  </div>
  <p class="px-6 pt-3 text-[.84rem] text-rv-muted max-sm:px-4" aria-live="polite" data-shown>Showing 6 of 6 reviews</p>
  <ol class="grid gap-3.5 p-6 pt-3.5 max-sm:p-4" data-list>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="5" data-verified="true" data-date="2026-10-12" data-helpful="24">
      <article aria-labelledby="rv-t1">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#fed7aa] text-[#7c2d12]" aria-hidden="true">AO</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Amara Okeke</b><span class="text-rv-muted">Bristol, UK</span></p><span class="inline-flex items-center gap-1 rounded-full bg-rv-soft px-2 py-0.5 text-[.74rem] font-bold text-rv-accent-dark"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t1">Carries a laptop and a rain jacket without sagging</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-10-12">12 Oct 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>Comfortable all day</dd></div></dl>
        <p class="mt-2 text-pretty">I cycle 9 km each way and the back panel stays dry. The sleeve takes a 15-inch laptop with room left for the charger.</p>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h1" data-helpful-group><span id="rv-h1">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="24"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>24</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="4" data-verified="true" data-date="2026-10-03" data-helpful="17">
      <article aria-labelledby="rv-t2">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#bfdbfe] text-[#1e3a8a]" aria-hidden="true">HD</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Henrik Dahl</b><span class="text-rv-muted">Bergen, Norway</span></p><span class="inline-flex items-center gap-1 rounded-full bg-rv-soft px-2 py-0.5 text-[.74rem] font-bold text-rv-accent-dark"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:4" role="img" aria-label="4 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t2">Great pack, but the hip belt is thin</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-10-03">3 Oct 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Hiking</dd></div><div><dt>Fit</dt><dd>Runs slightly long</dd></div></dl>
        <p class="mt-2 text-pretty">Light and well organised. On a full-day hike with 8 kg inside, the thin hip belt started to dig in after a few hours.</p>
        <div class="mt-3.5 rounded-r-[10px] border-l-[3px] border-rv-accent bg-[#f6f8f6] px-3.5 py-2.5 text-[.88rem] [&_b]:mb-0.5 [&_b]:block [&_b]:text-[.8rem]"><b>Response from Kestrel Outdoors</b>Thanks, Henrik. A padded hip belt that clips onto the same loops goes on sale in November, and existing owners can swap theirs for free.</div>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h2" data-helpful-group><span id="rv-h2">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="17"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>17</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="2"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>2</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="5" data-verified="false" data-date="2026-09-28" data-helpful="11">
      <article aria-labelledby="rv-t3">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#fbcfe8] text-[#831843]" aria-hidden="true">ML</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Mei Lin</b><span class="text-rv-muted">Singapore</span></p></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t3">Survived a week of monsoon rain</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-09-28">28 Sep 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="mt-2 text-pretty">The roll-top and taped seams kept my camera dry through a week of downpours in Kerala.</p>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h3" data-helpful-group><span id="rv-h3">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="11"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>11</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="3" data-verified="true" data-date="2026-09-20" data-helpful="9">
      <article aria-labelledby="rv-t4">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#e9d5ff] text-[#581c87]" aria-hidden="true">CM</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Carlos Mendes</b><span class="text-rv-muted">Porto, Portugal</span></p><span class="inline-flex items-center gap-1 rounded-full bg-rv-soft px-2 py-0.5 text-[.74rem] font-bold text-rv-accent-dark"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:3" role="img" aria-label="3 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t4">Solid build, awkward side pockets</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-09-20">20 Sep 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Day hikes</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="mt-2 text-pretty">A 1-litre bottle only fits in the side pocket when the main compartment is half empty.</p>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h4" data-helpful-group><span id="rv-h4">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="9"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>9</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="3"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>3</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="5" data-verified="true" data-date="2026-10-16" data-helpful="4">
      <article aria-labelledby="rv-t5">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#fde68a] text-[#713f12]" aria-hidden="true">FZ</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Fatima Zahra</b><span class="text-rv-muted">Rabat, Morocco</span></p><span class="inline-flex items-center gap-1 rounded-full bg-rv-soft px-2 py-0.5 text-[.74rem] font-bold text-rv-accent-dark"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:5" role="img" aria-label="5 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t5">Fits under a budget airline seat</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-10-16">16 Oct 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Travel</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="mt-2 text-pretty">Packed for three days in Lisbon and it still slid under the seat in front of me.</p>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h5" data-helpful-group><span id="rv-h5">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="4"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>4</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>0</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
    <li class="rounded-[14px] border border-rv-line p-5 max-sm:p-4" data-rating="2" data-verified="true" data-date="2026-10-08" data-helpful="6">
      <article aria-labelledby="rv-t6">
        <div class="flex flex-wrap items-center gap-2.5"><span class="grid size-9 flex-none place-items-center rounded-full text-[.74rem] font-bold bg-[#cffafe] text-[#164e63]" aria-hidden="true">TB</span><p class="min-w-0 flex-1 text-[.86rem] leading-[1.35] [&_b]:block [&_b]:text-[.92rem]"><b>Tom Becker</b><span class="text-rv-muted">Leipzig, Germany</span></p><span class="inline-flex items-center gap-1 rounded-full bg-rv-soft px-2 py-0.5 text-[.74rem] font-bold text-rv-accent-dark"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5 9-10"/></svg>Verified purchase</span></div>
        <div class="mt-3.5 flex flex-wrap items-center gap-x-2.5 gap-y-1"><span class="relative inline-block leading-none tracking-[2px] text-[#c5cec8] before:absolute before:inset-y-0 before:left-0 before:w-[calc(var(--r)/5*100%)] before:overflow-hidden before:text-rv-star before:content-['★★★★★'] text-[.95rem]" style="--r:2" role="img" aria-label="2 out of 5 stars">★★★★★</span><h3 class="text-[1rem] leading-[1.35] font-bold" id="rv-t6">Zip pull snapped in month two</h3><time class="text-[.82rem] text-rv-muted" datetime="2026-10-08">8 Oct 2026</time></div>
        <dl class="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[.82rem] [&_dd]:inline [&_dt]:inline [&_dt]:text-rv-muted [&_dt]:after:content-[':_']"><div><dt>Used for</dt><dd>Commuting</dd></div><div><dt>Fit</dt><dd>True to size</dd></div></dl>
        <p class="mt-2 text-pretty">Support sent a replacement pull within a week, which I appreciate, but it should not have broken so soon.</p>
        <div class="mt-3.5 flex flex-wrap items-center gap-2 border-t border-dashed border-rv-line pt-3.5 text-[.84rem] text-rv-muted" role="group" aria-labelledby="rv-h6" data-helpful-group><span id="rv-h6">Was this review helpful?</span>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="6"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>Yes <span data-n>6</span></button>
          <button class="inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-full border-[1.5px] border-[#7d8a82] bg-white px-3 text-[.82rem] font-semibold text-rv-ink tabular-nums transition-colors duration-150 hover:bg-rv-soft aria-pressed:border-rv-accent aria-pressed:bg-rv-soft aria-pressed:text-rv-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rv-accent" type="button" aria-pressed="false" data-base="1"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="transform:scaleY(-1)"><path d="M7 10v11H3V10zM7 10l4-8a3 3 0 0 1 3 3v4h6a2 2 0 0 1 2 2.3l-1.4 8A2 2 0 0 1 18.6 21H7"/></svg>No <span data-n>1</span></button>
          <span class="font-semibold text-rv-accent-dark" aria-live="polite" data-thanks></span></div>
      </article>
    </li>
  </ol>
</section></div>
CSS
/* Palette as theme colours. The stars are a before: layer clipped to --r; pressed states use aria-pressed:. */
@theme {
  --color-rv-accent:#15803d; --color-rv-accent-dark:#166534; --color-rv-soft:#ecfdf3; --color-rv-ink:#17201a;
  --color-rv-muted:#526057; --color-rv-line:#e1e8e3; --color-rv-star:#d97706;
}
JavaScript
/* Review list: filter chips (one pressed at a time), sorting, and helpful votes (Yes or No, click again to undo).
   The "Showing n of n" line and the thanks note are polite live regions. Frontend only: post votes to your backend where marked. */
document.querySelectorAll('[data-reviews]').forEach(function (root) {
  var list = root.querySelector('[data-list]'), items = Array.from(list.children), shown = root.querySelector('[data-shown]'), sort = root.querySelector('[data-sort]');
  function filter(by) {
    var n = 0;
    items.forEach(function (li) {
      var r = +li.dataset.rating;
      var ok = by === 'all' || (by === 'verified' ? li.dataset.verified === 'true' : by === 'low' ? r <= 3 : r === +by);
      li.hidden = !ok; if (ok) n++;
    });
    shown.textContent = 'Showing ' + n + ' of ' + items.length + ' reviews';
  }
  root.querySelectorAll('[data-filter]').forEach(function (b) {
    b.addEventListener('click', function () {
      root.querySelectorAll('[data-filter]').forEach(function (x) { x.setAttribute('aria-pressed', x === b); });
      filter(b.dataset.filter);
    });
  });
  sort.addEventListener('change', function () {
    var by = sort.value;
    items.sort(function (a, b) {
      if (by === 'new') return b.dataset.date.localeCompare(a.dataset.date);
      if (by === 'low') return a.dataset.rating - b.dataset.rating;
      return b.dataset.helpful - a.dataset.helpful;
    });
    items.forEach(function (li) { list.appendChild(li); });
  });
  root.querySelectorAll('[data-helpful-group]').forEach(function (g) {
    var thanks = g.querySelector('[data-thanks]');
    g.addEventListener('click', function (e) {
      var b = e.target.closest('button');
      if (!b) return;
      var on = b.getAttribute('aria-pressed') !== 'true';
      g.querySelectorAll('button').forEach(function (x) {
        var p = x === b && on;
        x.setAttribute('aria-pressed', p);
        x.querySelector('[data-n]').textContent = +x.dataset.base + (p ? 1 : 0);
      });
      thanks.textContent = on ? 'Thanks for your feedback.' : ''; // send the vote here
    });
  });
});

About this set

A product review list that treats reviews as comments people read and judge. A compact summary at the top gives the average, stars, the number of reviews and the share of buyers who would recommend the product, without a full distribution chart. Below it, filter chips for All, 5 stars, 4 stars, 3 stars or lower and Verified only work as a single-choice group, a Sort select offers Most helpful, Newest and Lowest rating, and a polite line reports how many reviews are showing. Each review card has an initials avatar, the reviewer’s name and town, a Verified purchase badge, a star rating, a headline, the date, a short description list of how the product was used and how it fits, and the review text. One review carries a response from the seller, set off with a coloured rule. Every card ends with Was this review helpful? and Yes and No buttons with counts; one vote can be pressed at a time, pressing it again undoes it, and a thank-you note appears. The stars are text glyphs with a gold copy clipped to the rating, so any value such as 4.4 works.

What’s included

  • Compact rating summary with a recommend percentage
  • Single-choice filter chips and a Sort select with a live result count
  • Verified purchase badges, usage details and a seller response
  • Helpful Yes and No votes with counts and aria-pressed
  • Fractional star ratings drawn with one CSS custom property

Accessibility

Each star rating is a role="img" element with a label such as 4 out of 5 stars. Filter chips and helpful votes are buttons with aria-pressed, the helpful buttons sit in a group named by its question, and the result count and thank-you note are polite live regions. Each review is an article named by its headline, the sort select has a visible label, and every control shows a focus outline.

Customise it

Change --rv-accent, --rv-star and --rv-line on .rv, and --r in each star element's style for the rating. Bootstrap uses the same variables with .btn, .badge and .form-select; in Tailwind edit the --color-rv-* theme colours.