Open demo

CSS code

HTML
<div><span class="lbl">Colour and size swatches with a sale price</span>
<ul class="sw">
  <li><article class="sw-card">
    <div class="sw-media">
      <img src="https://images.unsplash.com/photo-1556821840-3a63f95609a7?auto=format&fit=crop&w=700&q=70" alt="Person seen from behind wearing a grey hooded sweatshirt outdoors" width="700" height="700" loading="lazy">
      <span class="sw-tag">Sale</span>
    </div>
    <div class="sw-body">
      <div class="sw-head">
        <h3 class="sw-title"><a href="#">Merino Pullover Hoodie</a></h3>
        <p class="sw-price"><span class="sr-only">Sale price</span>$72 <s><span class="sr-only">Was</span> $96</s></p>
      </div>
      <p class="sw-rating"><span class="sw-stars" style="--r:4.6" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4.6 out of 5, 204 reviews</span><span aria-hidden="true">4.6 (204)</span></p>
      <fieldset class="sw-group"><legend>Colour</legend><div class="sw-opts">
        <label><input type="radio" name="hoodie-colour" value="heather" checked><i class="sw-dot" style="--c:#9ca3af"></i><span class="sr-only">Heather grey</span></label>
        <label><input type="radio" name="hoodie-colour" value="charcoal"><i class="sw-dot" style="--c:#374151"></i><span class="sr-only">Charcoal</span></label>
        <label><input type="radio" name="hoodie-colour" value="forest"><i class="sw-dot" style="--c:#166534"></i><span class="sr-only">Forest green</span></label>
        <label><input type="radio" name="hoodie-colour" value="oat"><i class="sw-dot" style="--c:#d6c7a8"></i><span class="sr-only">Oat</span></label>
      </div></fieldset>
      <fieldset class="sw-group"><legend>Size</legend><div class="sw-opts">
        <label><input type="radio" name="hoodie-size" value="xs" disabled><span class="sw-size">XS<span class="sr-only"> (sold out)</span></span></label>
        <label><input type="radio" name="hoodie-size" value="s"><span class="sw-size">S</span></label>
        <label><input type="radio" name="hoodie-size" value="m" checked><span class="sw-size">M</span></label>
        <label><input type="radio" name="hoodie-size" value="l"><span class="sw-size">L</span></label>
        <label><input type="radio" name="hoodie-size" value="xl"><span class="sw-size">XL</span></label>
      </div></fieldset>
      <button type="button" class="sw-add">Add<span class="sr-only"> Merino Pullover Hoodie</span> to cart</button>
    </div>
  </article></li>
  <li><article class="sw-card">
    <div class="sw-media">
      <img src="https://images.unsplash.com/photo-1560343090-f0409e92791a?auto=format&fit=crop&w=700&q=70" alt="Teal suede lace-up shoe balanced on a peach on a pale pink set" width="700" height="700" loading="lazy">
      <span class="sw-tag">Sale</span>
    </div>
    <div class="sw-body">
      <div class="sw-head">
        <h3 class="sw-title"><a href="#">Suede City Brogue</a></h3>
        <p class="sw-price"><span class="sr-only">Sale price</span>$119 <s><span class="sr-only">Was</span> $165</s></p>
      </div>
      <p class="sw-rating"><span class="sw-stars" style="--r:4.3" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4.3 out of 5, 76 reviews</span><span aria-hidden="true">4.3 (76)</span></p>
      <fieldset class="sw-group"><legend>Colour</legend><div class="sw-opts">
        <label><input type="radio" name="brogue-colour" value="teal" checked><i class="sw-dot" style="--c:#4f8a83"></i><span class="sr-only">Teal</span></label>
        <label><input type="radio" name="brogue-colour" value="tan"><i class="sw-dot" style="--c:#b07a4b"></i><span class="sr-only">Tan</span></label>
        <label><input type="radio" name="brogue-colour" value="navy"><i class="sw-dot" style="--c:#1e3a5f"></i><span class="sr-only">Navy</span></label>
      </div></fieldset>
      <fieldset class="sw-group"><legend>EU size</legend><div class="sw-opts">
        <label><input type="radio" name="brogue-size" value="40"><span class="sw-size">40</span></label>
        <label><input type="radio" name="brogue-size" value="41"><span class="sw-size">41</span></label>
        <label><input type="radio" name="brogue-size" value="42" checked><span class="sw-size">42</span></label>
        <label><input type="radio" name="brogue-size" value="43"><span class="sw-size">43</span></label>
        <label><input type="radio" name="brogue-size" value="44" disabled><span class="sw-size">44<span class="sr-only"> (sold out)</span></span></label>
      </div></fieldset>
      <button type="button" class="sw-add">Add<span class="sr-only"> Suede City Brogue</span> to cart</button>
    </div>
  </article></li>
</ul></div>

<div><span class="lbl">Compact cards with colour dots</span>
<ul class="sw is-compact">
  <li><article class="sw-card">
    <div class="sw-media">
      <img src="https://images.unsplash.com/photo-1600857062241-98e5dba7f214?auto=format&fit=crop&w=500&q=70" alt="Brown leather crossbody bag with a flap and a thin strap" width="500" height="500" loading="lazy">
      <span class="sw-tag">−30%</span>
    </div>
    <div class="sw-body">
      <div class="sw-head"><h3 class="sw-title"><a href="#">Saddle Crossbody Bag</a></h3></div>
      <p class="sw-price"><span class="sr-only">Sale price</span>$84 <s><span class="sr-only">Was</span> $120</s></p>
      <fieldset class="sw-group"><legend class="sr-only">Colour</legend><div class="sw-opts">
        <label><input type="radio" name="bag-colour" value="cognac" checked><i class="sw-dot" style="--c:#9a5b2e"></i><span class="sr-only">Cognac</span></label>
        <label><input type="radio" name="bag-colour" value="black"><i class="sw-dot" style="--c:#1c1917"></i><span class="sr-only">Black</span></label>
        <label><input type="radio" name="bag-colour" value="olive"><i class="sw-dot" style="--c:#5b6236"></i><span class="sr-only">Olive</span></label>
      </div></fieldset>
      <button type="button" class="sw-add">Add<span class="sr-only"> Saddle Crossbody Bag</span> to cart</button>
    </div>
  </article></li>
  <li><article class="sw-card">
    <div class="sw-media">
      <img src="https://images.unsplash.com/photo-1622560480605-d83c853bc5c3?auto=format&fit=crop&w=500&q=70" alt="Dusty rose leather backpack with a front pocket" width="500" height="500" loading="lazy">
      <span class="sw-tag">−15%</span>
    </div>
    <div class="sw-body">
      <div class="sw-head"><h3 class="sw-title"><a href="#">Roll-Top Leather Backpack</a></h3></div>
      <p class="sw-price"><span class="sr-only">Sale price</span>$161 <s><span class="sr-only">Was</span> $190</s></p>
      <fieldset class="sw-group"><legend class="sr-only">Colour</legend><div class="sw-opts">
        <label><input type="radio" name="pack-colour" value="rosewood" checked><i class="sw-dot" style="--c:#9f6b5e"></i><span class="sr-only">Rosewood</span></label>
        <label><input type="radio" name="pack-colour" value="cognac"><i class="sw-dot" style="--c:#92400e"></i><span class="sr-only">Cognac</span></label>
        <label><input type="radio" name="pack-colour" value="black"><i class="sw-dot" style="--c:#1c1917"></i><span class="sr-only">Black</span></label>
      </div></fieldset>
      <button type="button" class="sw-add">Add<span class="sr-only"> Roll-Top Leather Backpack</span> to cart</button>
    </div>
  </article></li>
  <li><article class="sw-card">
    <div class="sw-media">
      <img src="https://images.unsplash.com/photo-1592078615290-033ee584e267?auto=format&fit=crop&w=500&q=70" alt="Black moulded dining chair with oak legs on a blue-grey background" width="500" height="500" loading="lazy">
      <span class="sw-tag">−25%</span>
    </div>
    <div class="sw-body">
      <div class="sw-head"><h3 class="sw-title"><a href="#">Shell Dining Chair</a></h3></div>
      <p class="sw-price"><span class="sr-only">Sale price</span>$66 <s><span class="sr-only">Was</span> $88</s></p>
      <fieldset class="sw-group"><legend class="sr-only">Colour</legend><div class="sw-opts">
        <label><input type="radio" name="chair-colour" value="black" checked><i class="sw-dot" style="--c:#1c1917"></i><span class="sr-only">Black</span></label>
        <label><input type="radio" name="chair-colour" value="white"><i class="sw-dot" style="--c:#f8fafc"></i><span class="sr-only">White</span></label>
        <label><input type="radio" name="chair-colour" value="sage"><i class="sw-dot" style="--c:#9caf88"></i><span class="sr-only">Sage</span></label>
      </div></fieldset>
      <button type="button" class="sw-add">Add<span class="sr-only"> Shell Dining Chair</span> to cart</button>
    </div>
  </article></li>
</ul></div>
CSS
/* Swatch cards: colour and size pickers are real radio groups (fieldset + legend). The radios are visually
   hidden; the sibling <i> (colour) or <span> (size) draws the swatch, its checked ring and its focus outline. */
.sw{--sw-ink:#0f172a;--sw-muted:#475569;--sw-line:#e2e8f0;--sw-media:#e9eef4;--sw-sale:#be123c;--sw-star:#d97706;--sw-star-off:#cbd5e1;--sw-focus:#2563eb;
  display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:24px;list-style:none;margin:0;padding:0}
.sw.is-compact{grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:20px}
.sw-card{display:flex;flex-direction:column;height:100%;background:#fff;border-radius:20px;box-shadow:0 1px 2px rgba(15,23,42,.06),0 10px 30px -18px rgba(15,23,42,.35);overflow:hidden}
.sw-media{position:relative;aspect-ratio:1;background:var(--sw-media);overflow:hidden}
.sw-media img{display:block;width:100%;height:100%;object-fit:cover}
.sw-tag{position:absolute;top:14px;left:14px;padding:5px 11px;border-radius:8px;background:var(--sw-sale);color:#fff;font-size:.75rem;font-weight:700;letter-spacing:.03em;line-height:1.3}
.sw-body{display:flex;flex:1;flex-direction:column;gap:12px;padding:18px 20px 20px}
.sw-head{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:4px 12px}
.sw-title{margin:0;font-size:1.08rem;font-weight:700;line-height:1.3;letter-spacing:-.01em}
.sw-title a{color:var(--sw-ink);text-decoration:none}
.sw-title a:hover{text-decoration:underline;text-underline-offset:3px}
.sw-price{display:flex;align-items:baseline;gap:8px;margin:0;font-size:1.1rem;font-weight:700;color:var(--sw-sale)}
.is-compact .sw-price{margin-top:-6px}
.sw-price s{font-size:.875rem;font-weight:500;color:var(--sw-muted)}
.sw-rating{display:flex;align-items:center;gap:6px;margin:-6px 0 0;font-size:.8rem;color:var(--sw-muted)}
.sw-stars{--r:5;font-size:.9rem;letter-spacing:1px;line-height:1;color:transparent;
  background:linear-gradient(90deg,var(--sw-star) calc(var(--r) / 5 * 100%),var(--sw-star-off) 0);-webkit-background-clip:text;background-clip:text}
.sw-group{min-width:0;margin:0;padding:0;border:0}
.sw-group legend{margin-bottom:8px;padding:0;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--sw-muted)}
.sw-opts{display:flex;flex-wrap:wrap;gap:8px}
.sw-opts label{position:relative;cursor:pointer}
.sw-opts input{position:absolute;opacity:0;width:1px;height:1px;margin:0}
/* colour dot */
.sw-dot{display:block;width:28px;height:28px;border-radius:50%;background:var(--c);box-shadow:inset 0 0 0 1px rgba(15,23,42,.3);transition:box-shadow .15s}
.is-compact .sw-dot{width:22px;height:22px}
input:checked+.sw-dot{box-shadow:inset 0 0 0 1px rgba(15,23,42,.3),0 0 0 2px #fff,0 0 0 4px var(--sw-ink)}
input:focus-visible+.sw-dot{outline:3px solid var(--sw-focus);outline-offset:5px}
/* size pill */
.sw-size{display:grid;place-items:center;min-width:44px;height:38px;padding:0 10px;border:1.5px solid #94a3b8;border-radius:10px;
  font-size:.85rem;font-weight:600;color:var(--sw-ink);transition:background-color .15s,border-color .15s,color .15s}
label:hover input:enabled+.sw-size{border-color:var(--sw-ink)}
input:checked+.sw-size{background:var(--sw-ink);border-color:var(--sw-ink);color:#fff}
input:focus-visible+.sw-size{outline:3px solid var(--sw-focus);outline-offset:2px}
input:disabled+.sw-size{color:#64748b;border-style:dashed;text-decoration:line-through;cursor:not-allowed}
.sw-opts label:has(input:disabled){cursor:not-allowed}
.sw-add{margin-top:auto;padding:12px 16px;border:0;border-radius:12px;background:var(--sw-ink);color:#fff;font:inherit;font-size:.92rem;font-weight:600;cursor:pointer;transition:background-color .15s}
.sw-add:hover{background:#334155}
.is-compact .sw-add{padding:10px 14px;border:1.5px solid var(--sw-ink);background:transparent;color:var(--sw-ink)}
.is-compact .sw-add:hover{background:var(--sw-ink);color:#fff}
.sw :is(a,button):focus-visible{outline:3px solid var(--sw-focus);outline-offset:2px}
.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}
@media (prefers-reduced-motion:reduce){.sw-dot,.sw-size,.sw-add{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Colour and size swatches with a sale price</span>
<ul class="sw row row-cols-1 row-cols-md-2 list-unstyled mb-0">
  <li class="col"><article class="card h-100">
    <div class="position-relative"><div class="ratio ratio-1x1"><img src="https://images.unsplash.com/photo-1556821840-3a63f95609a7?auto=format&fit=crop&w=700&q=70" alt="Person seen from behind wearing a grey hooded sweatshirt outdoors" width="700" height="700" loading="lazy"></div>
      <span class="badge position-absolute">Sale</span></div>
    <div class="card-body d-flex flex-column">
      <div class="d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1">
        <h3 class="sw-title fw-bold mb-0"><a href="#" class="text-decoration-none">Merino Pullover Hoodie</a></h3>
        <p class="sw-price d-flex align-items-baseline gap-2 fw-bold mb-0"><span class="visually-hidden">Sale price</span>$72 <s><span class="visually-hidden">Was</span> $96</s></p>
      </div>
      <p class="sw-rating d-flex align-items-center gap-2 mb-0"><span class="sw-stars" style="--r:4.6" aria-hidden="true">★★★★★</span><span class="visually-hidden">Rated 4.6 out of 5, 204 reviews</span><span aria-hidden="true">4.6 (204)</span></p>
      <fieldset><legend>Colour</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="hoodie-colour" id="sw1" value="heather" checked><label class="sw-dot" for="sw1" style="--c:#9ca3af"><span class="visually-hidden">Heather grey</span></label>
        <input type="radio" class="btn-check" name="hoodie-colour" id="sw2" value="charcoal"None><label class="sw-dot" for="sw2" style="--c:#374151"><span class="visually-hidden">Charcoal</span></label>
        <input type="radio" class="btn-check" name="hoodie-colour" id="sw3" value="forest"None><label class="sw-dot" for="sw3" style="--c:#166534"><span class="visually-hidden">Forest green</span></label>
        <input type="radio" class="btn-check" name="hoodie-colour" id="sw4" value="oat"None><label class="sw-dot" for="sw4" style="--c:#d6c7a8"><span class="visually-hidden">Oat</span></label>
      </div></fieldset>
      <fieldset><legend>Size</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="hoodie-size" id="sw17" value="xs" disabled><label class="btn sw-size" for="sw17">XS<span class="visually-hidden"> (sold out)</span></label>
        <input type="radio" class="btn-check" name="hoodie-size" id="sw18" value="s"><label class="btn sw-size" for="sw18">S</label>
        <input type="radio" class="btn-check" name="hoodie-size" id="sw19" value="m" checked><label class="btn sw-size" for="sw19">M</label>
        <input type="radio" class="btn-check" name="hoodie-size" id="sw20" value="l"><label class="btn sw-size" for="sw20">L</label>
        <input type="radio" class="btn-check" name="hoodie-size" id="sw21" value="xl"><label class="btn sw-size" for="sw21">XL</label>
      </div></fieldset>
      <button type="button" class="btn sw-add mt-auto">Add<span class="visually-hidden"> Merino Pullover Hoodie</span> to cart</button>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="position-relative"><div class="ratio ratio-1x1"><img src="https://images.unsplash.com/photo-1560343090-f0409e92791a?auto=format&fit=crop&w=700&q=70" alt="Teal suede lace-up shoe balanced on a peach on a pale pink set" width="700" height="700" loading="lazy"></div>
      <span class="badge position-absolute">Sale</span></div>
    <div class="card-body d-flex flex-column">
      <div class="d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1">
        <h3 class="sw-title fw-bold mb-0"><a href="#" class="text-decoration-none">Suede City Brogue</a></h3>
        <p class="sw-price d-flex align-items-baseline gap-2 fw-bold mb-0"><span class="visually-hidden">Sale price</span>$119 <s><span class="visually-hidden">Was</span> $165</s></p>
      </div>
      <p class="sw-rating d-flex align-items-center gap-2 mb-0"><span class="sw-stars" style="--r:4.3" aria-hidden="true">★★★★★</span><span class="visually-hidden">Rated 4.3 out of 5, 76 reviews</span><span aria-hidden="true">4.3 (76)</span></p>
      <fieldset><legend>Colour</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="brogue-colour" id="sw5" value="teal" checked><label class="sw-dot" for="sw5" style="--c:#4f8a83"><span class="visually-hidden">Teal</span></label>
        <input type="radio" class="btn-check" name="brogue-colour" id="sw6" value="tan"None><label class="sw-dot" for="sw6" style="--c:#b07a4b"><span class="visually-hidden">Tan</span></label>
        <input type="radio" class="btn-check" name="brogue-colour" id="sw7" value="navy"None><label class="sw-dot" for="sw7" style="--c:#1e3a5f"><span class="visually-hidden">Navy</span></label>
      </div></fieldset>
      <fieldset><legend>EU size</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="brogue-size" id="sw22" value="40"><label class="btn sw-size" for="sw22">40</label>
        <input type="radio" class="btn-check" name="brogue-size" id="sw23" value="41"><label class="btn sw-size" for="sw23">41</label>
        <input type="radio" class="btn-check" name="brogue-size" id="sw24" value="42" checked><label class="btn sw-size" for="sw24">42</label>
        <input type="radio" class="btn-check" name="brogue-size" id="sw25" value="43"><label class="btn sw-size" for="sw25">43</label>
        <input type="radio" class="btn-check" name="brogue-size" id="sw26" value="44" disabled><label class="btn sw-size" for="sw26">44<span class="visually-hidden"> (sold out)</span></label>
      </div></fieldset>
      <button type="button" class="btn sw-add mt-auto">Add<span class="visually-hidden"> Suede City Brogue</span> to cart</button>
    </div>
  </article></li>
</ul></div>

<div><span class="lbl">Compact cards with colour dots</span>
<ul class="sw is-compact row row-cols-1 row-cols-sm-2 row-cols-md-3 list-unstyled mb-0">
  <li class="col"><article class="card h-100">
    <div class="position-relative"><div class="ratio ratio-1x1"><img src="https://images.unsplash.com/photo-1600857062241-98e5dba7f214?auto=format&fit=crop&w=500&q=70" alt="Brown leather crossbody bag with a flap and a thin strap" width="500" height="500" loading="lazy"></div>
      <span class="badge position-absolute">−30%</span></div>
    <div class="card-body d-flex flex-column">
      <div class="d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1"><h3 class="sw-title fw-bold mb-0"><a href="#" class="text-decoration-none">Saddle Crossbody Bag</a></h3></div>
      <p class="sw-price d-flex align-items-baseline gap-2 fw-bold mb-0"><span class="visually-hidden">Sale price</span>$84 <s><span class="visually-hidden">Was</span> $120</s></p>
      <fieldset><legend class="visually-hidden">Colour</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="bag-colour" id="sw8" value="cognac" checked><label class="sw-dot" for="sw8" style="--c:#9a5b2e"><span class="visually-hidden">Cognac</span></label>
        <input type="radio" class="btn-check" name="bag-colour" id="sw9" value="black"None><label class="sw-dot" for="sw9" style="--c:#1c1917"><span class="visually-hidden">Black</span></label>
        <input type="radio" class="btn-check" name="bag-colour" id="sw10" value="olive"None><label class="sw-dot" for="sw10" style="--c:#5b6236"><span class="visually-hidden">Olive</span></label>
      </div></fieldset>
      <button type="button" class="btn sw-add mt-auto">Add<span class="visually-hidden"> Saddle Crossbody Bag</span> to cart</button>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="position-relative"><div class="ratio ratio-1x1"><img src="https://images.unsplash.com/photo-1622560480605-d83c853bc5c3?auto=format&fit=crop&w=500&q=70" alt="Dusty rose leather backpack with a front pocket" width="500" height="500" loading="lazy"></div>
      <span class="badge position-absolute">−15%</span></div>
    <div class="card-body d-flex flex-column">
      <div class="d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1"><h3 class="sw-title fw-bold mb-0"><a href="#" class="text-decoration-none">Roll-Top Leather Backpack</a></h3></div>
      <p class="sw-price d-flex align-items-baseline gap-2 fw-bold mb-0"><span class="visually-hidden">Sale price</span>$161 <s><span class="visually-hidden">Was</span> $190</s></p>
      <fieldset><legend class="visually-hidden">Colour</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="pack-colour" id="sw11" value="rosewood" checked><label class="sw-dot" for="sw11" style="--c:#9f6b5e"><span class="visually-hidden">Rosewood</span></label>
        <input type="radio" class="btn-check" name="pack-colour" id="sw12" value="cognac"None><label class="sw-dot" for="sw12" style="--c:#92400e"><span class="visually-hidden">Cognac</span></label>
        <input type="radio" class="btn-check" name="pack-colour" id="sw13" value="black"None><label class="sw-dot" for="sw13" style="--c:#1c1917"><span class="visually-hidden">Black</span></label>
      </div></fieldset>
      <button type="button" class="btn sw-add mt-auto">Add<span class="visually-hidden"> Roll-Top Leather Backpack</span> to cart</button>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="position-relative"><div class="ratio ratio-1x1"><img src="https://images.unsplash.com/photo-1592078615290-033ee584e267?auto=format&fit=crop&w=500&q=70" alt="Black moulded dining chair with oak legs on a blue-grey background" width="500" height="500" loading="lazy"></div>
      <span class="badge position-absolute">−25%</span></div>
    <div class="card-body d-flex flex-column">
      <div class="d-flex flex-wrap align-items-baseline justify-content-between column-gap-3 row-gap-1"><h3 class="sw-title fw-bold mb-0"><a href="#" class="text-decoration-none">Shell Dining Chair</a></h3></div>
      <p class="sw-price d-flex align-items-baseline gap-2 fw-bold mb-0"><span class="visually-hidden">Sale price</span>$66 <s><span class="visually-hidden">Was</span> $88</s></p>
      <fieldset><legend class="visually-hidden">Colour</legend><div class="d-flex flex-wrap gap-2">
        <input type="radio" class="btn-check" name="chair-colour" id="sw14" value="black" checked><label class="sw-dot" for="sw14" style="--c:#1c1917"><span class="visually-hidden">Black</span></label>
        <input type="radio" class="btn-check" name="chair-colour" id="sw15" value="white"None><label class="sw-dot" for="sw15" style="--c:#f8fafc"><span class="visually-hidden">White</span></label>
        <input type="radio" class="btn-check" name="chair-colour" id="sw16" value="sage"None><label class="sw-dot" for="sw16" style="--c:#9caf88"><span class="visually-hidden">Sage</span></label>
      </div></fieldset>
      <button type="button" class="btn sw-add mt-auto">Add<span class="visually-hidden"> Shell Dining Chair</span> to cart</button>
    </div>
  </article></li>
</ul></div>
CSS
/* Swatch cards on Bootstrap: .card with a .ratio-1x1 photo; colour and size pickers are .btn-check radio
   groups inside a fieldset, sizes drawn as .btn pills, colours as round labels. */
.sw{--sw-ink:#0f172a;--sw-muted:#475569;--sw-sale:#be123c;--sw-star:#d97706;--sw-star-off:#cbd5e1;--sw-focus:#2563eb;--bs-gutter-x:24px;--bs-gutter-y:24px}
.sw.is-compact{--bs-gutter-x:20px;--bs-gutter-y:20px}
.sw .card{--bs-card-border-width:0;--bs-card-border-radius:20px;--bs-card-inner-border-radius:20px;--bs-card-spacer-x:20px;--bs-card-spacer-y:18px;
  overflow:hidden;box-shadow:0 1px 2px rgba(15,23,42,.06),0 10px 30px -18px rgba(15,23,42,.35)}
.sw .card-body{gap:12px;padding-bottom:20px}
.sw .ratio{background:#e9eef4}
.sw .ratio img{object-fit:cover}
.sw .badge{top:14px;left:14px;padding:5px 11px;border-radius:8px;font-size:.75rem;letter-spacing:.03em;line-height:1.3;background:var(--sw-sale)}
.sw-title{font-size:1.08rem;line-height:1.3;letter-spacing:-.01em}
.sw-title a{color:var(--sw-ink)}
.sw-title a:hover{text-decoration:underline!important;text-underline-offset:3px}
.sw-price{font-size:1.1rem;color:var(--sw-sale)}
.sw-price s{font-size:.875rem;font-weight:500;color:var(--sw-muted)}
.sw.is-compact .sw-price{margin-top:-6px}
.sw-rating{margin-top:-6px;font-size:.8rem;color:var(--sw-muted)}
.sw-stars{--r:5;font-size:.9rem;letter-spacing:1px;line-height:1;color:transparent;
  background:linear-gradient(90deg,var(--sw-star) calc(var(--r) / 5 * 100%),var(--sw-star-off) 0);-webkit-background-clip:text;background-clip:text}
.sw legend{float:none;width:auto;margin-bottom:8px;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--sw-muted);line-height:1.5}
.sw-dot{display:block;width:28px;height:28px;border-radius:50%;background:var(--c);box-shadow:inset 0 0 0 1px rgba(15,23,42,.3);cursor:pointer;transition:box-shadow .15s}
.is-compact .sw-dot{width:22px;height:22px}
.btn-check:checked+.sw-dot{box-shadow:inset 0 0 0 1px rgba(15,23,42,.3),0 0 0 2px #fff,0 0 0 4px var(--sw-ink)}
.btn-check:focus-visible+.sw-dot{outline:3px solid var(--sw-focus);outline-offset:5px}
.sw-size{--bs-btn-padding-x:10px;--bs-btn-padding-y:0;--bs-btn-font-size:.85rem;--bs-btn-font-weight:600;--bs-btn-border-width:1.5px;--bs-btn-border-radius:10px;
  --bs-btn-color:var(--sw-ink);--bs-btn-border-color:#94a3b8;--bs-btn-hover-color:var(--sw-ink);--bs-btn-hover-border-color:var(--sw-ink);
  --bs-btn-active-bg:var(--sw-ink);--bs-btn-active-border-color:var(--sw-ink);--bs-btn-active-color:#fff;--bs-btn-disabled-color:#64748b;--bs-btn-disabled-border-color:#94a3b8;--bs-btn-disabled-opacity:1;
  display:grid;place-items:center;min-width:44px;height:38px}
.btn-check:disabled+.sw-size{border-style:dashed;text-decoration:line-through}
.btn-check:focus-visible+.sw-size{outline:3px solid var(--sw-focus);outline-offset:2px;box-shadow:none}
.sw-add{--bs-btn-padding-y:12px;--bs-btn-font-size:.92rem;--bs-btn-font-weight:600;--bs-btn-border-radius:12px;--bs-btn-bg:var(--sw-ink);--bs-btn-border-color:var(--sw-ink);
  --bs-btn-color:#fff;--bs-btn-hover-bg:#334155;--bs-btn-hover-border-color:#334155;--bs-btn-hover-color:#fff;--bs-btn-active-bg:#334155;--bs-btn-active-border-color:#334155}
.is-compact .sw-add{--bs-btn-padding-y:10px;--bs-btn-border-width:1.5px;--bs-btn-bg:transparent;--bs-btn-color:var(--sw-ink);--bs-btn-hover-bg:var(--sw-ink);--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:var(--sw-ink)}
.sw :is(a,.sw-add):focus-visible{outline:3px solid var(--sw-focus);outline-offset:2px;box-shadow:none}
@media (prefers-reduced-motion:reduce){.sw-dot,.sw .btn{transition:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#475569] uppercase">Colour and size swatches with a sale price</span>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-6 max-sm:grid-cols-1">
  <li><article class="flex h-full flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_10px_30px_-18px_rgba(15,23,42,.35)]">
    <div class="relative aspect-square overflow-hidden bg-[#e9eef4]">
      <img class="size-full object-cover" src="https://images.unsplash.com/photo-1556821840-3a63f95609a7?auto=format&fit=crop&w=700&q=70" alt="Person seen from behind wearing a grey hooded sweatshirt outdoors" width="700" height="700" loading="lazy">
      <span class="absolute top-3.5 left-3.5 rounded-lg bg-[#be123c] px-[11px] py-[5px] text-xs leading-[1.3] font-bold tracking-[.03em] text-white">Sale</span>
    </div>
    <div class="flex flex-1 flex-col gap-3 px-5 pt-[18px] pb-5">
      <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
        <h3 class="text-[1.08rem] leading-[1.3] font-bold tracking-[-.01em]"><a href="#" class="text-[#0f172a] underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Merino Pullover Hoodie</a></h3>
        <p class="flex items-baseline gap-2 text-[1.1rem] font-bold text-[#be123c]"><span class="sr-only">Sale price</span>$72 <s class="text-[.875rem] font-medium text-[#475569]"><span class="sr-only">Was</span> $96</s></p>
      </div>
      <p class="-mt-1.5 flex items-center gap-1.5 text-[.8rem] text-[#475569]"><span class="bg-[linear-gradient(90deg,#d97706_92%,#cbd5e1_0)] bg-clip-text text-[.9rem] leading-none tracking-[1px] text-transparent" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4.6 out of 5, 204 reviews</span><span aria-hidden="true">4.6 (204)</span></p>
      <fieldset class="min-w-0"><legend class="mb-2 text-[.72rem] font-bold tracking-[.1em] text-[#475569] uppercase">Colour</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="hoodie-colour" value="heather" checked><i class="block size-7 rounded-full bg-[#9ca3af] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Heather grey</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="hoodie-colour" value="charcoal"><i class="block size-7 rounded-full bg-[#374151] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Charcoal</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="hoodie-colour" value="forest"><i class="block size-7 rounded-full bg-[#166534] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Forest green</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="hoodie-colour" value="oat"><i class="block size-7 rounded-full bg-[#d6c7a8] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Oat</span></label>
      </div></fieldset>
      <fieldset class="min-w-0"><legend class="mb-2 text-[.72rem] font-bold tracking-[.1em] text-[#475569] uppercase">Size</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="hoodie-size" value="xs" disabled><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">XS<span class="sr-only"> (sold out)</span></span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="hoodie-size" value="s"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">S</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="hoodie-size" value="m" checked><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">M</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="hoodie-size" value="l"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">L</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="hoodie-size" value="xl"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">XL</span></label>
      </div></fieldset>
      <button type="button" class="mt-auto cursor-pointer rounded-xl bg-[#0f172a] px-4 py-3 text-[.92rem] font-semibold text-white transition-colors duration-150 hover:bg-[#334155] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none">Add<span class="sr-only"> Merino Pullover Hoodie</span> to cart</button>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_10px_30px_-18px_rgba(15,23,42,.35)]">
    <div class="relative aspect-square overflow-hidden bg-[#e9eef4]">
      <img class="size-full object-cover" src="https://images.unsplash.com/photo-1560343090-f0409e92791a?auto=format&fit=crop&w=700&q=70" alt="Teal suede lace-up shoe balanced on a peach on a pale pink set" width="700" height="700" loading="lazy">
      <span class="absolute top-3.5 left-3.5 rounded-lg bg-[#be123c] px-[11px] py-[5px] text-xs leading-[1.3] font-bold tracking-[.03em] text-white">Sale</span>
    </div>
    <div class="flex flex-1 flex-col gap-3 px-5 pt-[18px] pb-5">
      <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
        <h3 class="text-[1.08rem] leading-[1.3] font-bold tracking-[-.01em]"><a href="#" class="text-[#0f172a] underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Suede City Brogue</a></h3>
        <p class="flex items-baseline gap-2 text-[1.1rem] font-bold text-[#be123c]"><span class="sr-only">Sale price</span>$119 <s class="text-[.875rem] font-medium text-[#475569]"><span class="sr-only">Was</span> $165</s></p>
      </div>
      <p class="-mt-1.5 flex items-center gap-1.5 text-[.8rem] text-[#475569]"><span class="bg-[linear-gradient(90deg,#d97706_86%,#cbd5e1_0)] bg-clip-text text-[.9rem] leading-none tracking-[1px] text-transparent" aria-hidden="true">★★★★★</span><span class="sr-only">Rated 4.3 out of 5, 76 reviews</span><span aria-hidden="true">4.3 (76)</span></p>
      <fieldset class="min-w-0"><legend class="mb-2 text-[.72rem] font-bold tracking-[.1em] text-[#475569] uppercase">Colour</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="brogue-colour" value="teal" checked><i class="block size-7 rounded-full bg-[#4f8a83] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Teal</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="brogue-colour" value="tan"><i class="block size-7 rounded-full bg-[#b07a4b] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Tan</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="brogue-colour" value="navy"><i class="block size-7 rounded-full bg-[#1e3a5f] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Navy</span></label>
      </div></fieldset>
      <fieldset class="min-w-0"><legend class="mb-2 text-[.72rem] font-bold tracking-[.1em] text-[#475569] uppercase">EU size</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="brogue-size" value="40"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">40</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="brogue-size" value="41"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">41</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="brogue-size" value="42" checked><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">42</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="brogue-size" value="43"><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">43</span></label>
        <label class="cursor-pointer has-disabled:cursor-not-allowed"><input class="peer sr-only" type="radio" name="brogue-size" value="44" disabled><span class="grid h-[38px] min-w-11 place-items-center rounded-[10px] border-[1.5px] border-[#94a3b8] px-2.5 text-[.85rem] font-semibold text-[#0f172a] transition-colors duration-150 peer-enabled:hover:border-[#0f172a] peer-checked:border-[#0f172a] peer-checked:bg-[#0f172a] peer-checked:text-white peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#2563eb] peer-disabled:border-dashed peer-disabled:text-[#64748b] peer-disabled:line-through motion-reduce:transition-none">44<span class="sr-only"> (sold out)</span></span></label>
      </div></fieldset>
      <button type="button" class="mt-auto cursor-pointer rounded-xl bg-[#0f172a] px-4 py-3 text-[.92rem] font-semibold text-white transition-colors duration-150 hover:bg-[#334155] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none">Add<span class="sr-only"> Suede City Brogue</span> to cart</button>
    </div>
  </article></li>
</ul></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#475569] uppercase">Compact cards with colour dots</span>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(230px,1fr))] gap-5">
  <li><article class="flex h-full flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_10px_30px_-18px_rgba(15,23,42,.35)]">
    <div class="relative aspect-square overflow-hidden bg-[#e9eef4]">
      <img class="size-full object-cover" src="https://images.unsplash.com/photo-1600857062241-98e5dba7f214?auto=format&fit=crop&w=500&q=70" alt="Brown leather crossbody bag with a flap and a thin strap" width="500" height="500" loading="lazy">
      <span class="absolute top-3.5 left-3.5 rounded-lg bg-[#be123c] px-[11px] py-[5px] text-xs leading-[1.3] font-bold tracking-[.03em] text-white">−30%</span>
    </div>
    <div class="flex flex-1 flex-col gap-3 px-5 pt-[18px] pb-5">
      <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><h3 class="text-[1.08rem] leading-[1.3] font-bold tracking-[-.01em]"><a href="#" class="text-[#0f172a] underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Saddle Crossbody Bag</a></h3></div>
      <p class="flex items-baseline gap-2 text-[1.1rem] font-bold text-[#be123c] -mt-1.5"><span class="sr-only">Sale price</span>$84 <s class="text-[.875rem] font-medium text-[#475569]"><span class="sr-only">Was</span> $120</s></p>
      <fieldset class="min-w-0"><legend class="sr-only">Colour</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="bag-colour" value="cognac" checked><i class="block size-[22px] rounded-full bg-[#9a5b2e] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Cognac</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="bag-colour" value="black"><i class="block size-[22px] rounded-full bg-[#1c1917] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Black</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="bag-colour" value="olive"><i class="block size-[22px] rounded-full bg-[#5b6236] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Olive</span></label>
      </div></fieldset>
      <button type="button" class="mt-auto cursor-pointer rounded-xl border-[1.5px] border-[#0f172a] px-3.5 py-2.5 text-[.92rem] font-semibold text-[#0f172a] transition-colors duration-150 hover:bg-[#0f172a] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none">Add<span class="sr-only"> Saddle Crossbody Bag</span> to cart</button>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_10px_30px_-18px_rgba(15,23,42,.35)]">
    <div class="relative aspect-square overflow-hidden bg-[#e9eef4]">
      <img class="size-full object-cover" src="https://images.unsplash.com/photo-1622560480605-d83c853bc5c3?auto=format&fit=crop&w=500&q=70" alt="Dusty rose leather backpack with a front pocket" width="500" height="500" loading="lazy">
      <span class="absolute top-3.5 left-3.5 rounded-lg bg-[#be123c] px-[11px] py-[5px] text-xs leading-[1.3] font-bold tracking-[.03em] text-white">−15%</span>
    </div>
    <div class="flex flex-1 flex-col gap-3 px-5 pt-[18px] pb-5">
      <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><h3 class="text-[1.08rem] leading-[1.3] font-bold tracking-[-.01em]"><a href="#" class="text-[#0f172a] underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Roll-Top Leather Backpack</a></h3></div>
      <p class="flex items-baseline gap-2 text-[1.1rem] font-bold text-[#be123c] -mt-1.5"><span class="sr-only">Sale price</span>$161 <s class="text-[.875rem] font-medium text-[#475569]"><span class="sr-only">Was</span> $190</s></p>
      <fieldset class="min-w-0"><legend class="sr-only">Colour</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="pack-colour" value="rosewood" checked><i class="block size-[22px] rounded-full bg-[#9f6b5e] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Rosewood</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="pack-colour" value="cognac"><i class="block size-[22px] rounded-full bg-[#92400e] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Cognac</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="pack-colour" value="black"><i class="block size-[22px] rounded-full bg-[#1c1917] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Black</span></label>
      </div></fieldset>
      <button type="button" class="mt-auto cursor-pointer rounded-xl border-[1.5px] border-[#0f172a] px-3.5 py-2.5 text-[.92rem] font-semibold text-[#0f172a] transition-colors duration-150 hover:bg-[#0f172a] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none">Add<span class="sr-only"> Roll-Top Leather Backpack</span> to cart</button>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_1px_2px_rgba(15,23,42,.06),0_10px_30px_-18px_rgba(15,23,42,.35)]">
    <div class="relative aspect-square overflow-hidden bg-[#e9eef4]">
      <img class="size-full object-cover" src="https://images.unsplash.com/photo-1592078615290-033ee584e267?auto=format&fit=crop&w=500&q=70" alt="Black moulded dining chair with oak legs on a blue-grey background" width="500" height="500" loading="lazy">
      <span class="absolute top-3.5 left-3.5 rounded-lg bg-[#be123c] px-[11px] py-[5px] text-xs leading-[1.3] font-bold tracking-[.03em] text-white">−25%</span>
    </div>
    <div class="flex flex-1 flex-col gap-3 px-5 pt-[18px] pb-5">
      <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><h3 class="text-[1.08rem] leading-[1.3] font-bold tracking-[-.01em]"><a href="#" class="text-[#0f172a] underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Shell Dining Chair</a></h3></div>
      <p class="flex items-baseline gap-2 text-[1.1rem] font-bold text-[#be123c] -mt-1.5"><span class="sr-only">Sale price</span>$66 <s class="text-[.875rem] font-medium text-[#475569]"><span class="sr-only">Was</span> $88</s></p>
      <fieldset class="min-w-0"><legend class="sr-only">Colour</legend><div class="flex flex-wrap gap-2">
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="chair-colour" value="black" checked><i class="block size-[22px] rounded-full bg-[#1c1917] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Black</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="chair-colour" value="white"><i class="block size-[22px] rounded-full bg-[#f8fafc] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">White</span></label>
        <label class="cursor-pointer"><input class="peer sr-only" type="radio" name="chair-colour" value="sage"><i class="block size-[22px] rounded-full bg-[#9caf88] shadow-[inset_0_0_0_1px_rgba(15,23,42,.3)] transition-shadow duration-150 peer-checked:shadow-[inset_0_0_0_1px_rgba(15,23,42,.3),0_0_0_2px_#fff,0_0_0_4px_#0f172a] peer-focus-visible:outline-3 peer-focus-visible:outline-offset-[5px] peer-focus-visible:outline-[#2563eb] motion-reduce:transition-none"></i><span class="sr-only">Sage</span></label>
      </div></fieldset>
      <button type="button" class="mt-auto cursor-pointer rounded-xl border-[1.5px] border-[#0f172a] px-3.5 py-2.5 text-[.92rem] font-semibold text-[#0f172a] transition-colors duration-150 hover:bg-[#0f172a] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none">Add<span class="sr-only"> Shell Dining Chair</span> to cart</button>
    </div>
  </article></li>
</ul></div>

About this set

Product cards that let the shopper choose a variant before adding to the cart, each with a sale price. Colour and size swatches with a sale price shows a hoodie and a pair of shoes on square photos with a Sale tag, the name and price on one line with the original price struck through, a star rating, a row of round colour swatches and a row of size pills, one of them sold out, then a full-width Add to cart button. Compact cards with colour dots is a three-up version for bags, backpacks and chairs: a discount tag, the sale price, small colour dots and an outline button. Every picker is a fieldset with a legend and a group of radio buttons, so one choice per group is enforced by the browser and the arrow keys move between options. The radios are visually hidden and a sibling element draws the swatch, a ring around the checked colour and a filled pill for the checked size. A sold-out size is a disabled radio drawn with a dashed border and a line through it. No JavaScript is needed. The Bootstrap version uses .btn-check with .btn pills for the sizes, and the Tailwind version uses peer-checked: and peer-disabled: utilities.

What’s included

  • Colour swatches and size pills built from real radio groups
  • Sold-out sizes as disabled radios with a dashed, struck-through pill
  • Sale price with the original price struck through
  • Arrow keys move between options, no JavaScript
  • Bootstrap version built on .btn-check and .btn

Accessibility

Each picker is a fieldset whose legend names it, Colour or Size, and every colour radio has a visually hidden name such as Heather grey, so screen readers hear the colour rather than an unlabelled button. Sold-out sizes are disabled and say sold out to screen readers. Focus shows as a 3px outline around the visible swatch or pill, the checked state is drawn with a ring or a filled background rather than colour alone, and transitions stop under prefers-reduced-motion.

Customise it

Set --sw-ink, --sw-sale and --sw-focus on .sw, and give each swatch its colour with --c. In Bootstrap restyle .sw-size through its --bs-btn-* variables; in Tailwind change the bg-[#…] colour of each swatch and the peer-checked: utilities.