Open demo

CSS code

HTML
<div><span class="lbl">Cart page with order summary</span>
<section class="cp" data-cart data-free-over="150" data-ship="6.95" aria-labelledby="cp1-title">
  <div class="cp-head"><h2 class="cp-title" id="cp1-title">Your cart</h2><span class="cp-count" data-count>4 items</span></div>
  <div class="cp-layout" data-filled>
    <div class="cp-card cp-lines">
      <div class="cp-cols" aria-hidden="true"><span>Product</span><span>Quantity</span><span>Total</span></div>
      <ul class="cp-list">
        <li class="cp-line" data-line data-name="Hearth mug" data-price="24.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Hearth mug</a></h3><p class="cp-meta">Oat glaze · 350 ml</p><p class="cp-each">$24.00 each</p></div>
          <div class="cp-qty"><label class="cp-qty-lbl" for="cp1-q1">Qty<span class="sr-only"> of Hearth mug</span></label>
            <div class="cp-step"><button class="cp-step-btn" type="button" data-dec aria-label="Decrease quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="cp-step-input" id="cp1-q1" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="cp-step-btn" type="button" data-inc aria-label="Increase quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$48.00</p>
          <button class="cp-remove" type="button" data-remove aria-label="Remove Hearth mug"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="cp-line" data-line data-name="Nesting cups, set of 4" data-price="38.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Nesting cups, set of 4</a></h3><p class="cp-meta">Speckled clay</p><p class="cp-each">$38.00 each</p></div>
          <div class="cp-qty"><label class="cp-qty-lbl" for="cp1-q2">Qty<span class="sr-only"> of Nesting cups, set of 4</span></label>
            <div class="cp-step"><button class="cp-step-btn" type="button" data-dec aria-label="Decrease quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="cp-step-input" id="cp1-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="cp-step-btn" type="button" data-inc aria-label="Increase quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$38.00</p>
          <button class="cp-remove" type="button" data-remove aria-label="Remove Nesting cups, set of 4"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="cp-line" data-line data-name="Bud vase trio" data-price="46.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Bud vase trio</a></h3><p class="cp-meta">Matte stone</p><p class="cp-each">$46.00 each</p></div>
          <div class="cp-qty"><label class="cp-qty-lbl" for="cp1-q3">Qty<span class="sr-only"> of Bud vase trio</span></label>
            <div class="cp-step"><button class="cp-step-btn" type="button" data-dec aria-label="Decrease quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="cp-step-input" id="cp1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="cp-step-btn" type="button" data-inc aria-label="Increase quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$46.00</p>
          <button class="cp-remove" type="button" data-remove aria-label="Remove Bud vase trio"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
      </ul>
    </div>
    <aside class="cp-card cp-sum" aria-labelledby="cp1-sum">
      <h3 class="cp-sum-title" id="cp1-sum">Order summary</h3>
      <dl class="cp-rows">
        <div class="cp-row"><dt>Subtotal</dt><dd data-subtotal>$132.00</dd></div>
        <div class="cp-row"><dt>Shipping</dt><dd data-shipping>$6.95</dd></div>
        <div class="cp-row"><dt>Tax</dt><dd>At checkout</dd></div>
      </dl>
      <dl><div class="cp-row cp-row--total"><dt>Total</dt><dd data-total>$138.95</dd></div></dl>
      <p class="cp-note">Free shipping on orders of $150.00 or more.</p>
      <a class="cp-btn" href="#">Go to checkout<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></a>
      <a class="cp-link" href="#">Continue shopping</a>
      <p class="cp-trust"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/></svg>30-day returns on unused items</p>
    </aside>
  </div>
  <div class="cp-card cp-empty" data-empty hidden>
    <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="cp-empty-title" tabindex="-1">Your cart is empty</h3>
    <p>Everything you add will wait here until you are ready to check out.</p>
    <div class="cp-empty-actions"><a class="cp-btn" href="#">Shop new arrivals</a></div>
  </div>
  <p class="sr-only" role="status" data-announce></p>
</section>
</div>
<div class="demo-row">
<div><span class="lbl">Compact cart</span>
<section class="cp cp-card cp-compact" data-cart data-free-over="50" data-ship="5.00" aria-labelledby="cp2-title">
  <div class="cp-head"><h2 class="cp-title" id="cp2-title">Your cart</h2><span class="cp-count" data-count>3 items</span></div>
  <div data-filled>
    <ul class="cp-list">
      <li class="cp-line" data-line data-name="Haworthia, mint pot" data-price="22.00">
        <img class="cp-img" src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="cp-info"><h3 class="cp-name"><a href="#">Haworthia, mint pot</a></h3><p class="cp-meta">12 cm pot · $22.00 each</p></div>
        <p class="cp-total" data-line-total>$22.00</p>
        <div class="cp-qty"><label class="cp-qty-lbl" for="cp2-q1">Qty<span class="sr-only"> of Haworthia, mint pot</span></label>
          <div class="cp-step"><button class="cp-step-btn" type="button" data-dec aria-label="Decrease quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="cp-step-input" id="cp2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="cp-step-btn" type="button" data-inc aria-label="Increase quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="cp-remove" type="button" data-remove aria-label="Remove Haworthia, mint pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
      <li class="cp-line" data-line data-name="Barrel cactus, terracotta" data-price="16.00">
        <img class="cp-img" src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="cp-info"><h3 class="cp-name"><a href="#">Barrel cactus, terracotta</a></h3><p class="cp-meta">9 cm pot · $16.00 each</p></div>
        <p class="cp-total" data-line-total>$32.00</p>
        <div class="cp-qty"><label class="cp-qty-lbl" for="cp2-q2">Qty<span class="sr-only"> of Barrel cactus, terracotta</span></label>
          <div class="cp-step"><button class="cp-step-btn" type="button" data-dec aria-label="Decrease quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="cp-step-input" id="cp2-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="cp-step-btn" type="button" data-inc aria-label="Increase quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="cp-remove" type="button" data-remove aria-label="Remove Barrel cactus, terracotta"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
    </ul>
    <dl class="cp-rows">
      <div class="cp-row"><dt>Subtotal</dt><dd data-subtotal>$54.00</dd></div>
      <div class="cp-row"><dt>Shipping <small>(free over $50.00)</small></dt><dd data-shipping>Free</dd></div>
    </dl>
    <dl><div class="cp-row cp-row--total"><dt>Total</dt><dd data-total>$54.00</dd></div></dl>
    <a class="cp-btn" href="#">Checkout</a>
  </div>
  <div class="cp-empty" data-empty hidden>
    <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="cp-empty-title" tabindex="-1">Your cart is empty</h3>
    <p>Your plants are still on the shelf, ready when you are.</p>
    <div class="cp-empty-actions"><a class="cp-btn" href="#">Browse plants</a></div>
  </div>
  <p class="sr-only" role="status" data-announce></p>
</section>
</div>
<div><span class="lbl">Empty cart</span>
<section class="cp cp-card cp-empty" aria-labelledby="cp3-title">
  <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
  <h2 class="cp-empty-title" id="cp3-title">Your cart is empty</h2>
  <p>Looks like you have not added anything yet. Our spring glazes are back in stock.</p>
  <div class="cp-empty-actions"><a class="cp-btn" href="#">Shop new arrivals</a><a class="cp-btn cp-btn--ghost" href="#">View saved items (2)</a></div>
</section>
</div>
</div>
CSS
/* Shopping cart pages. Each [data-cart] holds its own lines; the script reads data-price on each line and
   recalculates line totals, subtotal, shipping and total in cents, so the numbers always add up. */
.cp{--cp-accent:#2f5d46;--cp-accent-dark:#244a37;--cp-ink:#1f1b17;--cp-muted:#5e564e;--cp-line:#e4ddd3;--cp-field:#857b70;
  --cp-bg:#fff;--cp-soft:#f7f4ef;--cp-ring:rgba(47,93,70,.3);--cp-danger:#a8321c;
  color:var(--cp-ink);font-size:.93rem;line-height:1.45}
.cp [hidden]{display:none!important}
.cp .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}
.cp-card{background:var(--cp-bg);border-radius:18px;box-shadow:0 1px 2px rgba(31,27,23,.05),0 16px 40px -20px rgba(31,27,23,.28)}
.cp-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:18px}
.cp-title{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.9rem;letter-spacing:-.01em;line-height:1.1}
.cp-count{color:var(--cp-muted);font-size:.88rem}
.cp-layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:24px;align-items:start}
.cp-lines{padding:8px 26px}
.cp-cols{display:grid;grid-template-columns:minmax(0,1fr) 128px 84px 36px;gap:16px;padding:14px 0 12px;border-bottom:1px solid var(--cp-line);
  font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--cp-muted)}
.cp-cols span:nth-child(3){text-align:right}
.cp-list{list-style:none}
.cp-line{display:grid;grid-template-columns:88px minmax(0,1fr) 128px 84px 36px;grid-template-areas:"img info qty total rm";gap:16px;align-items:center;
  padding:18px 0;border-bottom:1px solid var(--cp-line)}
.cp-line:last-child{border-bottom:0}
.cp-img{grid-area:img;width:88px;height:88px;border-radius:12px;object-fit:cover;background:var(--cp-soft)}
.cp-info{grid-area:info;min-width:0}
.cp-name{font-size:1rem;font-weight:600;line-height:1.3}
.cp-name a{color:inherit;text-decoration:none}
.cp-name a:hover{text-decoration:underline;text-underline-offset:3px}
.cp-meta,.cp-each{color:var(--cp-muted);font-size:.84rem;margin-top:2px}
.cp-qty{grid-area:qty}
.cp-qty-lbl{display:block;font-size:.72rem;font-weight:600;color:var(--cp-muted);margin-bottom:4px}
.cp-step{display:inline-flex;align-items:center;border:1px solid var(--cp-field);border-radius:999px;background:#fff}
.cp-step:focus-within{border-color:var(--cp-accent);box-shadow:0 0 0 3px var(--cp-ring)}
.cp-step-btn{width:34px;height:34px;display:grid;place-items:center;border:0;background:none;color:var(--cp-ink);border-radius:50%;cursor:pointer;font:inherit}
.cp-step-btn:hover{background:var(--cp-soft)}
.cp-step-btn[aria-disabled="true"]{color:#a69d93;cursor:not-allowed;background:none}
.cp-step-input{width:38px;height:34px;border:0;background:none;text-align:center;font:inherit;font-weight:600;color:inherit;-moz-appearance:textfield;appearance:textfield}
.cp-step-input::-webkit-inner-spin-button,.cp-step-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.cp-step-input:focus{outline:none}
.cp-total{grid-area:total;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
.cp-remove{grid-area:rm;width:36px;height:36px;display:grid;place-items:center;border:0;background:none;color:var(--cp-muted);border-radius:50%;cursor:pointer}
.cp-remove:hover{color:var(--cp-danger);background:#fbeee9}
.cp-step-btn:focus-visible,.cp-remove:focus-visible,.cp a:focus-visible,.cp-btn:focus-visible,.cp-empty-title:focus-visible{outline:2px solid var(--cp-accent);outline-offset:2px}
.cp-empty-title:focus:not(:focus-visible){outline:none}
/* summary */
.cp-sum{padding:24px}
.cp-sum-title{font-size:1.05rem;font-weight:700;margin-bottom:14px}
.cp-rows{display:grid;gap:10px;padding-bottom:16px;border-bottom:1px solid var(--cp-line)}
.cp-row{display:flex;justify-content:space-between;gap:12px}
.cp-row dt{color:var(--cp-muted)}
.cp-row dd{font-weight:600;font-variant-numeric:tabular-nums;text-align:right}
.cp-row--total{padding-top:16px;font-size:1.12rem}
.cp-row--total dt{color:var(--cp-ink);font-weight:700}
.cp-row--total dd{font-weight:800}
.cp-note{font-size:.8rem;color:var(--cp-muted);margin:8px 0 18px}
.cp-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:13px 18px;border:0;border-radius:999px;background:var(--cp-accent);color:#fff;
  font:inherit;font-weight:700;text-decoration:none;cursor:pointer;transition:background-color .15s}
.cp-btn:hover{background:var(--cp-accent-dark)}
.cp-btn--ghost{background:none;color:var(--cp-accent);border:1px solid var(--cp-accent)}
.cp-btn--ghost:hover{background:var(--cp-soft)}
.cp-link{display:block;text-align:center;margin-top:12px;color:var(--cp-accent);font-weight:600;font-size:.88rem;text-underline-offset:3px}
.cp-trust{display:flex;gap:8px;align-items:center;justify-content:center;margin-top:16px;color:var(--cp-muted);font-size:.78rem}
/* compact cart */
.cp-compact{padding:22px}
.cp-compact .cp-head{margin-bottom:6px}
.cp-compact .cp-title{font-size:1.4rem}
.cp-compact .cp-line{grid-template-columns:64px minmax(0,1fr) auto;grid-template-areas:"img info total" "img qty rm";gap:6px 14px;padding:16px 0}
.cp-compact .cp-img{width:64px;height:64px;border-radius:10px}
.cp-compact .cp-qty{display:flex;align-items:center;gap:8px}
.cp-compact .cp-qty-lbl{margin:0}
.cp-compact .cp-step-btn{width:30px;height:30px}
.cp-compact .cp-step-input{height:30px;width:32px}
.cp-compact .cp-total{align-self:start}
.cp-compact .cp-remove{justify-self:end}
.cp-compact .cp-rows{border-top:1px solid var(--cp-line);padding-top:14px;margin-top:2px}
.cp-compact .cp-btn{margin-top:16px}
/* empty state */
.cp-empty{text-align:center;padding:40px 26px 34px}
.cp-empty-art{width:76px;height:76px;margin:0 auto 18px;border-radius:50%;background:var(--cp-soft);display:grid;place-items:center;color:var(--cp-accent)}
.cp-empty-title{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.45rem;margin-bottom:6px}
.cp-empty p{color:var(--cp-muted);max-width:30ch;margin:0 auto 20px}
.cp-empty-actions{display:grid;gap:10px;max-width:260px;margin:0 auto}
@media (max-width:820px){.cp-layout{grid-template-columns:1fr}}
@media (max-width:600px){
  .cp-lines{padding:4px 18px}
  .cp-cols{display:none}
  .cp-line{grid-template-columns:72px minmax(0,1fr) auto;grid-template-areas:"img info rm" "img qty total";gap:8px 14px}
  .cp-img{width:72px;height:72px}
  .cp-total{align-self:end;padding-bottom:6px}
}
@media (prefers-reduced-motion:reduce){.cp-btn{transition:none}}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Cart page with order summary</span>
<section class="cp" data-cart data-free-over="150" data-ship="6.95" aria-labelledby="cp1-title">
  <div class="d-flex align-items-baseline justify-content-between gap-3 mb-3"><h2 class="cp-title" id="cp1-title">Your cart</h2><span class="text-body-secondary small" data-count>4 items</span></div>
  <div class="cp-layout" data-filled>
    <div class="card cp-lines"><div class="card-body">
      <div class="cp-cols" aria-hidden="true"><span>Product</span><span>Quantity</span><span>Total</span></div>
      <ul class="list-group">
        <li class="list-group-item cp-line" data-line data-name="Hearth mug" data-price="24.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Hearth mug</a></h3><p class="text-body-secondary small mt-1">Oat glaze · 350 ml</p><p class="text-body-secondary small">$24.00 each</p></div>
          <div class="cp-qty"><label class="form-label d-block" for="cp1-q1">Qty<span class="visually-hidden"> of Hearth mug</span></label>
            <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="cp1-q1" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$48.00</p>
          <button class="btn cp-remove" type="button" data-remove aria-label="Remove Hearth mug"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="list-group-item cp-line" data-line data-name="Nesting cups, set of 4" data-price="38.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Nesting cups, set of 4</a></h3><p class="text-body-secondary small mt-1">Speckled clay</p><p class="text-body-secondary small">$38.00 each</p></div>
          <div class="cp-qty"><label class="form-label d-block" for="cp1-q2">Qty<span class="visually-hidden"> of Nesting cups, set of 4</span></label>
            <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="cp1-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$38.00</p>
          <button class="btn cp-remove" type="button" data-remove aria-label="Remove Nesting cups, set of 4"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="list-group-item cp-line" data-line data-name="Bud vase trio" data-price="46.00">
          <img class="cp-img" src="https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="cp-info"><h3 class="cp-name"><a href="#">Bud vase trio</a></h3><p class="text-body-secondary small mt-1">Matte stone</p><p class="text-body-secondary small">$46.00 each</p></div>
          <div class="cp-qty"><label class="form-label d-block" for="cp1-q3">Qty<span class="visually-hidden"> of Bud vase trio</span></label>
            <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="cp1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="cp-total" data-line-total>$46.00</p>
          <button class="btn cp-remove" type="button" data-remove aria-label="Remove Bud vase trio"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
      </ul>
    </div></div>
    <aside class="card cp-sum" aria-labelledby="cp1-sum"><div class="card-body">
      <h3 class="cp-sum-title" id="cp1-sum">Order summary</h3>
      <dl class="cp-rows">
        <div class="cp-row"><dt>Subtotal</dt><dd data-subtotal>$132.00</dd></div>
        <div class="cp-row"><dt>Shipping</dt><dd data-shipping>$6.95</dd></div>
        <div class="cp-row"><dt>Tax</dt><dd>At checkout</dd></div>
      </dl>
      <dl><div class="cp-row cp-row--total"><dt>Total</dt><dd data-total>$138.95</dd></div></dl>
      <p class="text-body-secondary mt-2 mb-3" style="font-size:.8rem">Free shipping on orders of $150.00 or more.</p>
      <a class="btn btn-primary" href="#">Go to checkout<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></a>
      <a class="cp-link" href="#">Continue shopping</a>
      <p class="d-flex gap-2 align-items-center justify-content-center mt-3 text-body-secondary" style="font-size:.78rem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/></svg>30-day returns on unused items</p>
    </div></aside>
  </div>
  <div class="card cp-empty" data-empty hidden><div class="card-body">
    <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="cp-empty-title" tabindex="-1">Your cart is empty</h3>
    <p>Everything you add will wait here until you are ready to check out.</p>
    <div class="cp-empty-actions"><a class="btn btn-primary" href="#">Shop new arrivals</a></div>
  </div></div>
  <p class="visually-hidden" role="status" data-announce></p>
</section>
</div>
<div class="demo-row">
<div><span class="lbl">Compact cart</span>
<section class="cp card cp-compact" data-cart data-free-over="50" data-ship="5.00" aria-labelledby="cp2-title"><div class="card-body">
  <div class="d-flex align-items-baseline justify-content-between gap-3 mb-1"><h2 class="cp-title" id="cp2-title">Your cart</h2><span class="text-body-secondary small" data-count>3 items</span></div>
  <div data-filled>
    <ul class="list-group">
      <li class="list-group-item cp-line" data-line data-name="Haworthia, mint pot" data-price="22.00">
        <img class="cp-img" src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="cp-info"><h3 class="cp-name"><a href="#">Haworthia, mint pot</a></h3><p class="text-body-secondary small mt-1">12 cm pot · $22.00 each</p></div>
        <p class="cp-total" data-line-total>$22.00</p>
        <div class="cp-qty"><label class="form-label" for="cp2-q1">Qty<span class="visually-hidden"> of Haworthia, mint pot</span></label>
          <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="cp2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="btn cp-remove" type="button" data-remove aria-label="Remove Haworthia, mint pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
      <li class="list-group-item cp-line" data-line data-name="Barrel cactus, terracotta" data-price="16.00">
        <img class="cp-img" src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="cp-info"><h3 class="cp-name"><a href="#">Barrel cactus, terracotta</a></h3><p class="text-body-secondary small mt-1">9 cm pot · $16.00 each</p></div>
        <p class="cp-total" data-line-total>$32.00</p>
        <div class="cp-qty"><label class="form-label" for="cp2-q2">Qty<span class="visually-hidden"> of Barrel cactus, terracotta</span></label>
          <div class="input-group"><button class="btn" type="button" data-dec aria-label="Decrease quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="form-control" id="cp2-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="btn" type="button" data-inc aria-label="Increase quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="btn cp-remove" type="button" data-remove aria-label="Remove Barrel cactus, terracotta"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
    </ul>
    <dl class="cp-rows">
      <div class="cp-row"><dt>Subtotal</dt><dd data-subtotal>$54.00</dd></div>
      <div class="cp-row"><dt>Shipping <small>(free over $50.00)</small></dt><dd data-shipping>Free</dd></div>
    </dl>
    <dl><div class="cp-row cp-row--total"><dt>Total</dt><dd data-total>$54.00</dd></div></dl>
    <a class="btn btn-primary mt-3" href="#">Checkout</a>
  </div>
  <div class="cp-empty py-4" data-empty hidden>
    <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="cp-empty-title" tabindex="-1">Your cart is empty</h3>
    <p>Your plants are still on the shelf, ready when you are.</p>
    <div class="cp-empty-actions"><a class="btn btn-primary" href="#">Browse plants</a></div>
  </div>
  <p class="visually-hidden" role="status" data-announce></p>
</div></section>
</div>
<div><span class="lbl">Empty cart</span>
<section class="cp card cp-empty" aria-labelledby="cp3-title"><div class="card-body">
  <div class="cp-empty-art"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
  <h2 class="cp-empty-title" id="cp3-title">Your cart is empty</h2>
  <p>Looks like you have not added anything yet. Our spring glazes are back in stock.</p>
  <div class="cp-empty-actions"><a class="btn btn-primary" href="#">Shop new arrivals</a><a class="btn btn-outline-primary" href="#">View saved items (2)</a></div>
</div></section>
</div>
</div>
CSS
/* Bootstrap .card, .list-group, .input-group, .form-control and .btn, recoloured through Bootstrap's CSS
   variables. The same script as the plain version recalculates every total from data-price. */
.cp{--cp-accent:#2f5d46;--cp-accent-dark:#244a37;--cp-ink:#1f1b17;--cp-muted:#5e564e;--cp-line:#e4ddd3;--cp-field:#857b70;--cp-soft:#f7f4ef;
  --cp-ring:rgba(47,93,70,.3);--cp-danger:#a8321c;
  --bs-body-color:var(--cp-ink);--bs-secondary-color:var(--cp-muted);--bs-border-color:var(--cp-line);
  --bs-link-color-rgb:47,93,70;--bs-link-hover-color-rgb:36,74,55;
  color:var(--cp-ink);font-size:.93rem;line-height:1.45}
.cp p,.cp h2,.cp h3,.cp dl,.cp dd{margin:0}
.cp .card{--bs-card-border-width:0;--bs-card-border-radius:18px;--bs-card-spacer-x:26px;--bs-card-spacer-y:8px;
  box-shadow:0 1px 2px rgba(31,27,23,.05),0 16px 40px -20px rgba(31,27,23,.28)}
.cp-title{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.9rem;letter-spacing:-.01em;line-height:1.1}
.cp-layout{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:24px;align-items:start}
.cp-cols{display:grid;grid-template-columns:minmax(0,1fr) 128px 84px 36px;gap:16px;padding:14px 0 12px;border-bottom:1px solid var(--cp-line);
  font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--cp-muted)}
.cp-cols span:nth-child(3){text-align:right}
.cp .list-group{--bs-list-group-border-width:0;--bs-list-group-item-padding-x:0;--bs-list-group-item-padding-y:18px;--bs-list-group-bg:transparent;--bs-list-group-color:var(--cp-ink)}
.cp-line{display:grid!important;grid-template-columns:88px minmax(0,1fr) 128px 84px 36px;grid-template-areas:"img info qty total rm";gap:16px;align-items:center;
  border-bottom:1px solid var(--cp-line)!important}
.cp-line:last-child{border-bottom:0!important}
.cp-img{grid-area:img;width:88px;height:88px;border-radius:12px;object-fit:cover;background:var(--cp-soft)}
.cp-info{grid-area:info;min-width:0}
.cp-name{font-size:1rem;font-weight:600;line-height:1.3}
.cp-name a{color:inherit;text-decoration:none}
.cp-name a:hover{text-decoration:underline;text-underline-offset:3px}
.cp-qty{grid-area:qty}
.cp .form-label{font-size:.72rem;font-weight:600;color:var(--cp-muted);margin-bottom:4px}
.cp .input-group{width:auto;display:inline-flex;flex-wrap:nowrap;border:1px solid var(--cp-field);border-radius:999px;background:#fff}
.cp .input-group:focus-within{border-color:var(--cp-accent);box-shadow:0 0 0 3px var(--cp-ring)}
.cp .input-group .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:var(--cp-ink);--bs-btn-hover-bg:var(--cp-soft);--bs-btn-hover-color:var(--cp-ink);
  --bs-btn-active-bg:var(--cp-line);--bs-btn-border-width:0;width:34px;height:34px;display:grid;place-items:center;border-radius:50%!important;z-index:0}
.cp .input-group .btn[aria-disabled="true"]{color:#a69d93;cursor:not-allowed;background:none}
.cp .input-group .form-control{flex:none;width:38px;height:34px;padding:0;border:0;background:none;text-align:center;font-weight:600;color:inherit;
  -moz-appearance:textfield;appearance:textfield;box-shadow:none}
.cp .input-group .form-control::-webkit-inner-spin-button,.cp .input-group .form-control::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.cp-total{grid-area:total;text-align:right;font-weight:700;font-variant-numeric:tabular-nums}
.cp-remove{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:var(--cp-muted);--bs-btn-hover-color:var(--cp-danger);--bs-btn-hover-bg:#fbeee9;--bs-btn-border-width:0;
  grid-area:rm;width:36px;height:36px;display:grid;place-items:center;border-radius:50%}
.cp .btn:focus-visible,.cp a:focus-visible,.cp-empty-title:focus-visible{outline:2px solid var(--cp-accent);outline-offset:2px;box-shadow:none}
.cp-empty-title:focus:not(:focus-visible){outline:none}
.cp-sum{--bs-card-spacer-x:24px!important;--bs-card-spacer-y:24px!important}
.cp-sum-title{font-size:1.05rem;font-weight:700;margin-bottom:14px!important}
.cp-rows{display:grid;gap:10px;padding-bottom:16px;border-bottom:1px solid var(--cp-line)}
.cp-row{display:flex;justify-content:space-between;gap:12px}
.cp-row dt{color:var(--cp-muted);font-weight:400}
.cp-row dd{font-weight:600;font-variant-numeric:tabular-nums;text-align:right}
.cp-row--total{padding-top:16px;font-size:1.12rem}
.cp-row--total dt{color:var(--cp-ink);font-weight:700}
.cp-row--total dd{font-weight:800}
.cp .btn-primary{--bs-btn-bg:var(--cp-accent);--bs-btn-border-color:var(--cp-accent);--bs-btn-hover-bg:var(--cp-accent-dark);--bs-btn-hover-border-color:var(--cp-accent-dark);
  --bs-btn-active-bg:var(--cp-accent-dark);--bs-btn-active-border-color:var(--cp-accent-dark);--bs-btn-padding-y:13px;--bs-btn-font-weight:700;--bs-btn-border-radius:999px;
  display:flex;align-items:center;justify-content:center;gap:8px;width:100%}
.cp .btn-outline-primary{--bs-btn-color:var(--cp-accent);--bs-btn-border-color:var(--cp-accent);--bs-btn-hover-bg:var(--cp-soft);--bs-btn-hover-color:var(--cp-accent);
  --bs-btn-hover-border-color:var(--cp-accent);--bs-btn-active-bg:var(--cp-line);--bs-btn-active-color:var(--cp-accent);--bs-btn-active-border-color:var(--cp-accent);
  --bs-btn-padding-y:12px;--bs-btn-font-weight:700;--bs-btn-border-radius:999px;width:100%}
.cp-link{display:block;text-align:center;margin-top:12px;font-weight:600;font-size:.88rem;text-underline-offset:3px}
.cp-compact{--bs-card-spacer-x:22px!important;--bs-card-spacer-y:22px!important}
.cp-compact .cp-title{font-size:1.4rem}
.cp-compact .cp-line{grid-template-columns:64px minmax(0,1fr) auto;grid-template-areas:"img info total" "img qty rm";gap:6px 14px;--bs-list-group-item-padding-y:16px}
.cp-compact .cp-img{width:64px;height:64px;border-radius:10px}
.cp-compact .cp-qty{display:flex;align-items:center;gap:8px}
.cp-compact .form-label{margin:0}
.cp-compact .input-group .btn{width:30px;height:30px}
.cp-compact .input-group .form-control{height:30px;width:32px}
.cp-compact .cp-total{align-self:start}
.cp-compact .cp-remove{justify-self:end}
.cp-compact .cp-rows{border-top:1px solid var(--cp-line);padding-top:14px;margin-top:2px}
.cp-empty{text-align:center;--bs-card-spacer-x:26px!important;--bs-card-spacer-y:40px!important}
.cp-empty-art{width:76px;height:76px;margin:0 auto 18px;border-radius:50%;background:var(--cp-soft);display:grid;place-items:center;color:var(--cp-accent)}
.cp-empty-title{font-family:Georgia,"Times New Roman",serif;font-weight:400;font-size:1.45rem;margin-bottom:6px!important}
.cp-empty p{color:var(--cp-muted);max-width:30ch;margin:0 auto 20px}
.cp-empty-actions{display:grid;gap:10px;max-width:260px;margin:0 auto}
@media (max-width:820px){.cp-layout{grid-template-columns:1fr}}
@media (max-width:600px){
  .cp-lines{--bs-card-spacer-x:18px}
  .cp-cols{display:none}
  .cp-line{grid-template-columns:72px minmax(0,1fr) auto;grid-template-areas:"img info rm" "img qty total";gap:8px 14px}
  .cp-img{width:72px;height:72px}
  .cp-total{align-self:end;padding-bottom:6px}
}
@media (prefers-reduced-motion:reduce){.cp .btn{transition:none}}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a6158] uppercase">Cart page with order summary</span>
<section class="text-[.93rem] leading-[1.45] text-cp-ink" data-cart data-free-over="150" data-ship="6.95" aria-labelledby="cp1-title">
  <div class="mb-[18px] flex items-baseline justify-between gap-3"><h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.9rem] leading-[1.1] font-normal tracking-[-.01em]" id="cp1-title">Your cart</h2><span class="text-[.88rem] text-cp-muted" data-count>4 items</span></div>
  <div class="grid grid-cols-[minmax(0,1fr)_300px] items-start gap-6 max-[820px]:grid-cols-1" data-filled>
    <div class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(31,27,23,.05),0_16px_40px_-20px_rgba(31,27,23,.28)] px-[26px] py-2 max-[600px]:px-[18px] max-[600px]:py-1">
      <div class="grid grid-cols-[minmax(0,1fr)_128px_84px_36px] gap-4 border-b border-cp-line pt-3.5 pb-3 text-[.72rem] tracking-[.1em] text-cp-muted uppercase max-[600px]:hidden" aria-hidden="true"><span>Product</span><span>Quantity</span><span class="text-right">Total</span></div>
      <ul>
        <li class="grid grid-cols-[88px_minmax(0,1fr)_128px_84px_36px] [grid-template-areas:'img_info_qty_total_rm'] items-center gap-4 border-b border-cp-line py-[18px] last:border-b-0 max-[600px]:grid-cols-[72px_minmax(0,1fr)_auto] max-[600px]:gap-x-3.5 max-[600px]:gap-y-2 max-[600px]:[grid-template-areas:'img_info_rm'_'img_qty_total']" data-line data-name="Hearth mug" data-price="24.00">
          <img class="[grid-area:img] size-[88px] rounded-xl bg-cp-soft object-cover max-[600px]:size-[72px]" src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="[grid-area:info] min-w-0"><h3 class="text-base leading-[1.3] font-semibold"><a class="hover:underline hover:underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Hearth mug</a></h3><p class="mt-0.5 text-[.84rem] text-cp-muted">Oat glaze · 350 ml</p><p class="mt-0.5 text-[.84rem] text-cp-muted">$24.00 each</p></div>
          <div class="[grid-area:qty]"><label class="mb-1 block text-[.72rem] font-semibold text-cp-muted" for="cp1-q1">Qty<span class="sr-only"> of Hearth mug</span></label>
            <div class="inline-flex items-center rounded-full border border-cp-field bg-white focus-within:border-cp-accent focus-within:shadow-[0_0_0_3px_rgba(47,93,70,.3)]"><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-dec aria-label="Decrease quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[34px] w-[38px] appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="cp1-q1" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-inc aria-label="Increase quantity of Hearth mug"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="[grid-area:total] text-right font-bold tabular-nums max-[600px]:self-end max-[600px]:pb-1.5" data-line-total>$48.00</p>
          <button class="[grid-area:rm] grid size-9 cursor-pointer place-items-center rounded-full text-cp-muted hover:bg-[#fbeee9] hover:text-cp-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-remove aria-label="Remove Hearth mug"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="grid grid-cols-[88px_minmax(0,1fr)_128px_84px_36px] [grid-template-areas:'img_info_qty_total_rm'] items-center gap-4 border-b border-cp-line py-[18px] last:border-b-0 max-[600px]:grid-cols-[72px_minmax(0,1fr)_auto] max-[600px]:gap-x-3.5 max-[600px]:gap-y-2 max-[600px]:[grid-template-areas:'img_info_rm'_'img_qty_total']" data-line data-name="Nesting cups, set of 4" data-price="38.00">
          <img class="[grid-area:img] size-[88px] rounded-xl bg-cp-soft object-cover max-[600px]:size-[72px]" src="https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="[grid-area:info] min-w-0"><h3 class="text-base leading-[1.3] font-semibold"><a class="hover:underline hover:underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Nesting cups, set of 4</a></h3><p class="mt-0.5 text-[.84rem] text-cp-muted">Speckled clay</p><p class="mt-0.5 text-[.84rem] text-cp-muted">$38.00 each</p></div>
          <div class="[grid-area:qty]"><label class="mb-1 block text-[.72rem] font-semibold text-cp-muted" for="cp1-q2">Qty<span class="sr-only"> of Nesting cups, set of 4</span></label>
            <div class="inline-flex items-center rounded-full border border-cp-field bg-white focus-within:border-cp-accent focus-within:shadow-[0_0_0_3px_rgba(47,93,70,.3)]"><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-dec aria-label="Decrease quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[34px] w-[38px] appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="cp1-q2" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-inc aria-label="Increase quantity of Nesting cups, set of 4"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="[grid-area:total] text-right font-bold tabular-nums max-[600px]:self-end max-[600px]:pb-1.5" data-line-total>$38.00</p>
          <button class="[grid-area:rm] grid size-9 cursor-pointer place-items-center rounded-full text-cp-muted hover:bg-[#fbeee9] hover:text-cp-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-remove aria-label="Remove Nesting cups, set of 4"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
        <li class="grid grid-cols-[88px_minmax(0,1fr)_128px_84px_36px] [grid-template-areas:'img_info_qty_total_rm'] items-center gap-4 border-b border-cp-line py-[18px] last:border-b-0 max-[600px]:grid-cols-[72px_minmax(0,1fr)_auto] max-[600px]:gap-x-3.5 max-[600px]:gap-y-2 max-[600px]:[grid-template-areas:'img_info_rm'_'img_qty_total']" data-line data-name="Bud vase trio" data-price="46.00">
          <img class="[grid-area:img] size-[88px] rounded-xl bg-cp-soft object-cover max-[600px]:size-[72px]" src="https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=176&h=176&fit=crop&auto=format&q=70" alt="" width="88" height="88">
          <div class="[grid-area:info] min-w-0"><h3 class="text-base leading-[1.3] font-semibold"><a class="hover:underline hover:underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Bud vase trio</a></h3><p class="mt-0.5 text-[.84rem] text-cp-muted">Matte stone</p><p class="mt-0.5 text-[.84rem] text-cp-muted">$46.00 each</p></div>
          <div class="[grid-area:qty]"><label class="mb-1 block text-[.72rem] font-semibold text-cp-muted" for="cp1-q3">Qty<span class="sr-only"> of Bud vase trio</span></label>
            <div class="inline-flex items-center rounded-full border border-cp-field bg-white focus-within:border-cp-accent focus-within:shadow-[0_0_0_3px_rgba(47,93,70,.3)]"><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-dec aria-label="Decrease quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[34px] w-[38px] appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="cp1-q3" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="size-[34px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-inc aria-label="Increase quantity of Bud vase trio"><svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
          <p class="[grid-area:total] text-right font-bold tabular-nums max-[600px]:self-end max-[600px]:pb-1.5" data-line-total>$46.00</p>
          <button class="[grid-area:rm] grid size-9 cursor-pointer place-items-center rounded-full text-cp-muted hover:bg-[#fbeee9] hover:text-cp-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-remove aria-label="Remove Bud vase trio"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
        </li>
      </ul>
    </div>
    <aside class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(31,27,23,.05),0_16px_40px_-20px_rgba(31,27,23,.28)] p-6" aria-labelledby="cp1-sum">
      <h3 class="mb-3.5 text-[1.05rem] font-bold" id="cp1-sum">Order summary</h3>
      <dl class="grid gap-2.5 border-b border-cp-line pb-4">
        <div class="flex justify-between gap-3"><dt class="text-cp-muted">Subtotal</dt><dd class="text-right font-semibold tabular-nums" data-subtotal>$132.00</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-cp-muted">Shipping</dt><dd class="text-right font-semibold tabular-nums" data-shipping>$6.95</dd></div>
        <div class="flex justify-between gap-3"><dt class="text-cp-muted">Tax</dt><dd class="text-right font-semibold">At checkout</dd></div>
      </dl>
      <dl><div class="flex justify-between gap-3 pt-4 text-[1.12rem]"><dt class="font-bold">Total</dt><dd class="text-right font-extrabold tabular-nums" data-total>$138.95</dd></div></dl>
      <p class="mt-2 mb-[18px] text-[.8rem] text-cp-muted">Free shipping on orders of $150.00 or more.</p>
      <a class="flex w-full items-center justify-center gap-2 rounded-full bg-cp-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-cp-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Go to checkout<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></a>
      <a class="mt-3 block text-center text-[.88rem] font-semibold text-cp-accent underline underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Continue shopping</a>
      <p class="mt-4 flex items-center justify-center gap-2 text-[.78rem] text-cp-muted"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8-8 9-4.6-1-8-4.5-8-9V6z"/></svg>30-day returns on unused items</p>
    </aside>
  </div>
  <div class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(31,27,23,.05),0_16px_40px_-20px_rgba(31,27,23,.28)] px-[26px] pt-10 pb-[34px] text-center" data-empty hidden>
    <div class="mx-auto mb-[18px] grid size-[76px] place-items-center rounded-full bg-cp-soft text-cp-accent"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="font-[Georgia,'Times_New_Roman',serif] text-[1.45rem] font-normal mb-1.5 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" tabindex="-1">Your cart is empty</h3>
    <p class="mx-auto mb-5 max-w-[30ch] text-cp-muted">Everything you add will wait here until you are ready to check out.</p>
    <div class="mx-auto grid max-w-[260px] gap-2.5"><a class="flex w-full items-center justify-center gap-2 rounded-full bg-cp-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-cp-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Shop new arrivals</a></div>
  </div>
  <p class="sr-only" role="status" data-announce></p>
</section>
</div>
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] items-start gap-[34px]">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a6158] uppercase">Compact cart</span>
<section class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(31,27,23,.05),0_16px_40px_-20px_rgba(31,27,23,.28)] p-[22px] text-[.93rem] leading-[1.45] text-cp-ink" data-cart data-free-over="50" data-ship="5.00" aria-labelledby="cp2-title">
  <div class="mb-1.5 flex items-baseline justify-between gap-3"><h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.4rem] leading-[1.1] font-normal tracking-[-.01em]" id="cp2-title">Your cart</h2><span class="text-[.88rem] text-cp-muted" data-count>3 items</span></div>
  <div data-filled>
    <ul>
      <li class="grid grid-cols-[64px_minmax(0,1fr)_auto] [grid-template-areas:'img_info_total'_'img_qty_rm'] gap-x-3.5 gap-y-1.5 border-b border-cp-line py-4 last:border-b-0" data-line data-name="Haworthia, mint pot" data-price="22.00">
        <img class="[grid-area:img] size-16 rounded-[10px] bg-cp-soft object-cover" src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="[grid-area:info] min-w-0"><h3 class="text-base leading-[1.3] font-semibold"><a class="hover:underline hover:underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Haworthia, mint pot</a></h3><p class="mt-0.5 text-[.84rem] text-cp-muted">12 cm pot · $22.00 each</p></div>
        <p class="[grid-area:total] self-start text-right font-bold tabular-nums" data-line-total>$22.00</p>
        <div class="[grid-area:qty] flex items-center gap-2"><label class="block text-[.72rem] font-semibold text-cp-muted" for="cp2-q1">Qty<span class="sr-only"> of Haworthia, mint pot</span></label>
          <div class="inline-flex items-center rounded-full border border-cp-field bg-white focus-within:border-cp-accent focus-within:shadow-[0_0_0_3px_rgba(47,93,70,.3)]"><button class="size-[30px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-dec aria-label="Decrease quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="cp2-q1" type="number" inputmode="numeric" min="1" max="10" value="1" data-qty><button class="size-[30px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-inc aria-label="Increase quantity of Haworthia, mint pot"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="[grid-area:rm] justify-self-end grid size-9 cursor-pointer place-items-center rounded-full text-cp-muted hover:bg-[#fbeee9] hover:text-cp-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-remove aria-label="Remove Haworthia, mint pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
      <li class="grid grid-cols-[64px_minmax(0,1fr)_auto] [grid-template-areas:'img_info_total'_'img_qty_rm'] gap-x-3.5 gap-y-1.5 border-b border-cp-line py-4 last:border-b-0" data-line data-name="Barrel cactus, terracotta" data-price="16.00">
        <img class="[grid-area:img] size-16 rounded-[10px] bg-cp-soft object-cover" src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?w=128&h=128&fit=crop&auto=format&q=70" alt="" width="64" height="64">
        <div class="[grid-area:info] min-w-0"><h3 class="text-base leading-[1.3] font-semibold"><a class="hover:underline hover:underline-offset-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Barrel cactus, terracotta</a></h3><p class="mt-0.5 text-[.84rem] text-cp-muted">9 cm pot · $16.00 each</p></div>
        <p class="[grid-area:total] self-start text-right font-bold tabular-nums" data-line-total>$32.00</p>
        <div class="[grid-area:qty] flex items-center gap-2"><label class="block text-[.72rem] font-semibold text-cp-muted" for="cp2-q2">Qty<span class="sr-only"> of Barrel cactus, terracotta</span></label>
          <div class="inline-flex items-center rounded-full border border-cp-field bg-white focus-within:border-cp-accent focus-within:shadow-[0_0_0_3px_rgba(47,93,70,.3)]"><button class="size-[30px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-dec aria-label="Decrease quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button><input class="h-[30px] w-8 appearance-none bg-transparent text-center font-semibold [-moz-appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" id="cp2-q2" type="number" inputmode="numeric" min="1" max="10" value="2" data-qty><button class="size-[30px] grid cursor-pointer place-items-center rounded-full text-cp-ink hover:bg-cp-soft aria-disabled:cursor-not-allowed aria-disabled:text-[#a69d93] aria-disabled:hover:bg-transparent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-inc aria-label="Increase quantity of Barrel cactus, terracotta"><svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M7 2v10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button></div></div>
        <button class="[grid-area:rm] justify-self-end grid size-9 cursor-pointer place-items-center rounded-full text-cp-muted hover:bg-[#fbeee9] hover:text-cp-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" type="button" data-remove aria-label="Remove Barrel cactus, terracotta"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg></button>
      </li>
    </ul>
    <dl class="mt-0.5 grid gap-2.5 border-y border-cp-line pt-3.5 pb-4">
      <div class="flex justify-between gap-3"><dt class="text-cp-muted">Subtotal</dt><dd class="text-right font-semibold tabular-nums" data-subtotal>$54.00</dd></div>
      <div class="flex justify-between gap-3"><dt class="text-cp-muted">Shipping <small>(free over $50.00)</small></dt><dd class="text-right font-semibold tabular-nums" data-shipping>Free</dd></div>
    </dl>
    <dl><div class="flex justify-between gap-3 pt-4 text-[1.12rem]"><dt class="font-bold">Total</dt><dd class="text-right font-extrabold tabular-nums" data-total>$54.00</dd></div></dl>
    <a class="mt-4 flex w-full items-center justify-center gap-2 rounded-full bg-cp-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-cp-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Checkout</a>
  </div>
  <div class="px-[26px] pt-10 pb-[34px] text-center" data-empty hidden>
    <div class="mx-auto mb-[18px] grid size-[76px] place-items-center rounded-full bg-cp-soft text-cp-accent"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
    <h3 class="font-[Georgia,'Times_New_Roman',serif] text-[1.45rem] font-normal mb-1.5 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" tabindex="-1">Your cart is empty</h3>
    <p class="mx-auto mb-5 max-w-[30ch] text-cp-muted">Your plants are still on the shelf, ready when you are.</p>
    <div class="mx-auto grid max-w-[260px] gap-2.5"><a class="flex w-full items-center justify-center gap-2 rounded-full bg-cp-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-cp-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Browse plants</a></div>
  </div>
  <p class="sr-only" role="status" data-announce></p>
</section>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6a6158] uppercase">Empty cart</span>
<section class="rounded-[18px] bg-white shadow-[0_1px_2px_rgba(31,27,23,.05),0_16px_40px_-20px_rgba(31,27,23,.28)] px-[26px] pt-10 pb-[34px] text-center text-[.93rem] leading-[1.45] text-cp-ink" aria-labelledby="cp3-title">
  <div class="mx-auto mb-[18px] grid size-[76px] place-items-center rounded-full bg-cp-soft text-cp-accent"><svg width="34" height="34" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 8h14l-1.2 11.1a2 2 0 0 1-2 1.9H8.2a2 2 0 0 1-2-1.9z"/><path d="M9 8V6a3 3 0 0 1 6 0v2"/></svg></div>
  <h2 class="font-[Georgia,'Times_New_Roman',serif] text-[1.45rem] font-normal mb-1.5 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" id="cp3-title">Your cart is empty</h2>
  <p class="mx-auto mb-5 max-w-[30ch] text-cp-muted">Looks like you have not added anything yet. Our spring glazes are back in stock.</p>
  <div class="mx-auto grid max-w-[260px] gap-2.5"><a class="flex w-full items-center justify-center gap-2 rounded-full bg-cp-accent px-[18px] py-[13px] font-bold text-white no-underline transition-colors duration-150 hover:bg-cp-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">Shop new arrivals</a><a class="flex w-full items-center justify-center rounded-full border border-cp-accent px-[18px] py-3 font-bold text-cp-accent hover:bg-cp-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cp-accent" href="#">View saved items (2)</a></div>
</section>
</div>
</div>
CSS
@theme {
  --color-cp-accent: #2f5d46;
  --color-cp-accent-dark: #244a37;
  --color-cp-ink: #1f1b17;
  --color-cp-muted: #5e564e;
  --color-cp-line: #e4ddd3;
  --color-cp-field: #857b70;
  --color-cp-soft: #f7f4ef;
  --color-cp-danger: #a8321c;
}
JavaScript
// Cart: quantity steppers, remove buttons and totals for every [data-cart] on the page. Prices come from
// data-price on each [data-line] and are summed in cents. Shipping is data-ship unless the subtotal reaches
// data-free-over. Changes are announced through the cart's polite [data-announce] region.
document.querySelectorAll('[data-cart]').forEach(function (cart) {
  var money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  var cents = function (v) { return Math.round(parseFloat(v) * 100); };
  var freeOver = cart.dataset.freeOver ? cents(cart.dataset.freeOver) : Infinity;
  var fee = cents(cart.dataset.ship || 0);
  var status = cart.querySelector('[data-announce]');
  function put(sel, text) { cart.querySelectorAll(sel).forEach(function (el) { el.textContent = text; }); }
  function render() {
    var sub = 0, count = 0;
    cart.querySelectorAll('[data-line]').forEach(function (line) {
      var input = line.querySelector('[data-qty]'), min = +input.min || 1, max = +input.max || 99;
      var qty = Math.min(max, Math.max(min, parseInt(input.value, 10) || min));
      var total = cents(line.dataset.price) * qty;
      input.value = qty; sub += total; count += qty;
      line.querySelector('[data-line-total]').textContent = money.format(total / 100);
      line.querySelector('[data-dec]').setAttribute('aria-disabled', qty <= min);
      line.querySelector('[data-inc]').setAttribute('aria-disabled', qty >= max);
    });
    var ship = sub === 0 || sub >= freeOver ? 0 : fee;
    put('[data-subtotal]', money.format(sub / 100));
    put('[data-shipping]', ship ? money.format(ship / 100) : 'Free');
    put('[data-total]', money.format((sub + ship) / 100));
    put('[data-count]', count + (count === 1 ? ' item' : ' items'));
    cart.querySelectorAll('[data-filled]').forEach(function (el) { el.hidden = !count; });
    cart.querySelectorAll('[data-empty]').forEach(function (el) { el.hidden = !!count; });
    return { sub: sub, count: count };
  }
  function announce(text) { if (status) status.textContent = text; }
  cart.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-inc],[data-dec],[data-remove]');
    if (!btn) return;
    var line = btn.closest('[data-line]'), name = line.dataset.name, input = line.querySelector('[data-qty]');
    if (btn.hasAttribute('data-remove')) {
      var next = line.nextElementSibling || line.previousElementSibling;
      line.remove();
      var r = render();
      announce('Removed ' + name + '. ' + (r.count ? 'Subtotal ' + money.format(r.sub / 100) + '.' : 'Your cart is empty.'));
      var target = r.count ? next.querySelector('[data-remove]') : cart.querySelector('[data-empty] [tabindex="-1"]');
      if (target) target.focus();
      return;
    }
    if (btn.getAttribute('aria-disabled') === 'true') return;
    input.value = (parseInt(input.value, 10) || 1) + (btn.hasAttribute('data-inc') ? 1 : -1);
    var s = render();
    announce(name + ', quantity ' + input.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  cart.addEventListener('change', function (e) {
    if (!e.target.matches('[data-qty]')) return;
    var s = render();
    announce(e.target.closest('[data-line]').dataset.name + ', quantity ' + e.target.value + '. Subtotal ' + money.format(s.sub / 100) + '.');
  });
  render();
});

About this set

Three shopping cart layouts for a home goods shop. Cart page with order summary is the full-width view: a product list with photo, name, variant, unit price, a quantity stepper, a line total and a remove button, beside a summary card with subtotal, shipping, a tax note, the total and a checkout button. Below 820 pixels the summary moves under the list, and below 600 each line folds into two rows. Compact cart is a single narrow card for a plant shop with smaller lines, a shipping line that turns free over $50.00 and one checkout button, which suits a sidebar or a phone. Empty cart is the state people see when nothing is left, with an icon, a short message and two ways back to the shop. A few lines of vanilla JavaScript run the steppers and remove buttons. Prices sit in a data-price attribute on each line and every total is recalculated in cents, so the numbers always add up, and shipping switches to free when the subtotal reaches the data-free-over amount. Removing the last line swaps in the empty state and moves focus to its heading, and each change is announced through a polite status region.

What’s included

  • Full cart page, compact cart and empty state
  • Quantity steppers with a visible Qty label, minus and plus buttons and a number input
  • Line totals, subtotal, shipping and total recalculated in cents from data-price
  • Free-shipping threshold set with data-free-over and data-ship
  • Removing the last item shows the empty state and moves focus to it

Accessibility

Each quantity input has a visible Qty label with the product name added as visually hidden text, and the minus, plus and remove buttons are named for their product, such as Remove Hearth mug. At the limits the stepper buttons get aria-disabled and stay focusable. Every change is announced through a polite role="status" region, and every control has a visible focus outline.

Customise it

Change --cp-accent, --cp-accent-dark, --cp-line and --cp-field on .cp for colour, and the grid-template-columns on .cp-line for column widths. In Bootstrap the same variables sit on .cp alongside the --bs-btn-* overrides; in Tailwind edit the cp-* colours in @theme.