Tailwind + JSMIT licensed

Tailwind Shopping Cart Table with Quantity Stepper & Live Totals

A light, warm checkout cart that actually works: steppers recalculate every line, the subtotal, shipping and total; removing an item swipes the row out and collapses it; and a free-shipping progress bar tells the shopper how far they are from the threshold. The summary moves beside the table at wide container sizes — the layout every Tier-1 storefront uses.

Live Demo
Plain CSSTailwind v4
Try it

Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

The code

<section class="ttb-05 grid-cols-1 w-full min-h-svh grid place-items-center bg-ttb05-bg px-5 py-14 font-ttb05-sans text-ttb05-ink" aria-label="Shopping cart table demo">
  <div id="ttb05-cart" class="group/cart @container w-full max-w-5xl">
    <header class="mb-6 flex flex-wrap items-end justify-between gap-3">
      <div>
        <h2 class="text-2xl font-extrabold tracking-[-0.02em]">Your cart</h2>
        <p class="mt-1 text-sm text-ttb05-mut"><span data-count>4 items</span> · ships in 1–2 business days</p>
      </div>
      <a href="#" class="text-sm font-semibold text-ttb05-acc-ink underline decoration-ttb05-acc/40 underline-offset-4 hover:decoration-ttb05-acc">Continue shopping</a>
    </header>
    <div class="grid grid-cols-1 items-start gap-6 @3xl:grid-cols-[minmax(0,1fr)_20rem]">
      <div class="relative min-w-0 overflow-x-auto rounded-2xl bg-white shadow-[0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0.4_0.05_50/0.18)] ring-1 ring-ttb05-line">
        <table class="w-full border-collapse text-left text-sm group-data-empty/cart:hidden">
          <caption class="sr-only">Items in your shopping cart</caption>
          <thead class="[&_th]:border-b [&_th]:border-ttb05-line [&_th]:px-5 [&_th]:py-3.5 [&_th]:text-[11px] [&_th]:font-bold [&_th]:tracking-[0.12em] [&_th]:text-ttb05-mut [&_th]:uppercase">
            <tr><th scope="col">Product</th><th scope="col" class="text-right max-sm:hidden">Price</th><th scope="col" class="text-center">Qty</th><th scope="col" class="text-right">Total</th><th scope="col"><span class="sr-only">Remove</span></th></tr>
          </thead>
          <tbody class="[&_td]:px-5 [&_td]:py-5 [&_td]:align-middle max-sm:[&_td]:px-2.5 max-sm:[&_td:first-child]:pl-4">
            <tr data-price="249" data-name="ProSound ANC Headphones" class="border-b border-ttb05-line transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-amber-200 to-amber-400 text-sm font-extrabold text-amber-900/70 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">ANC</span><div><div class="font-bold">ProSound ANC Headphones</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Midnight black · PS-ANC-900</div><span class="mt-1.5 inline-block rounded bg-ttb05-acc/10 px-1.5 py-0.5 text-[11px] font-semibold text-ttb05-acc-ink">In stock</span></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$249.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of ProSound ANC Headphones" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="1" aria-label="Quantity of ProSound ANC Headphones" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of ProSound ANC Headphones" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$249.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove ProSound ANC Headphones" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
            <tr data-price="129" data-name="MechKey RGB Keyboard" class="border-b border-ttb05-line transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-indigo-200 to-indigo-400 text-sm font-extrabold text-indigo-950/60 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">TKL</span><div><div class="font-bold">MechKey RGB Keyboard</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Arctic white · MK-RGB-TKL</div><span class="mt-1.5 inline-block rounded bg-ttb05-acc/10 px-1.5 py-0.5 text-[11px] font-semibold text-ttb05-acc-ink">Best seller</span></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$129.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of MechKey RGB Keyboard" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="2" aria-label="Quantity of MechKey RGB Keyboard" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of MechKey RGB Keyboard" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$258.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove MechKey RGB Keyboard" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
            <tr data-price="79" data-name="ErgoClick Pro Mouse" class="transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-emerald-200 to-emerald-400 text-sm font-extrabold text-emerald-950/60 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">G5</span><div><div class="font-bold">ErgoClick Pro Mouse</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Graphite · EC-PRO-G502</div></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$79.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of ErgoClick Pro Mouse" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="1" aria-label="Quantity of ErgoClick Pro Mouse" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of ErgoClick Pro Mouse" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$79.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove ErgoClick Pro Mouse" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
          </tbody>
        </table>
        <p class="hidden px-6 py-16 text-center text-ttb05-mut group-data-empty/cart:block">Your cart is empty. <a href="#" class="font-semibold text-ttb05-acc-ink underline underline-offset-4">Browse best sellers</a></p>
      </div>
      <aside class="rounded-2xl bg-white p-6 shadow-[0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0.4_0.05_50/0.18)] ring-1 ring-ttb05-line" aria-label="Order summary">
        <h3 class="text-base font-extrabold">Order summary</h3>
        <div class="mt-4 rounded-xl bg-ttb05-bg p-3.5">
          <p data-ship-msg class="text-xs font-semibold text-ttb05-ink">You’ve unlocked free express shipping</p>
          <div class="mt-2 h-1.5 overflow-hidden rounded-full bg-ttb05-line"><div data-bar class="h-full w-full rounded-full bg-linear-to-r from-ttb05-acc to-amber-400 transition-[width] duration-500 motion-reduce:transition-none"></div></div>
        </div>
        <dl class="mt-5 grid gap-3 text-sm [&_dd]:text-right [&_dd]:font-semibold [&_dd]:tabular-nums [&_dt]:text-ttb05-mut">
          <div class="flex justify-between"><dt>Subtotal</dt><dd data-subtotal>$586.00</dd></div>
          <div class="flex justify-between"><dt>Shipping</dt><dd data-shipping class="text-emerald-700">Free</dd></div>
          <div class="flex justify-between border-t border-ttb05-line pt-3 text-base"><dt class="font-bold text-ttb05-ink!">Total</dt><dd data-total class="text-lg font-extrabold">$586.00</dd></div>
        </dl>
        <a href="#" class="mt-6 flex min-h-12 items-center justify-center gap-2 rounded-xl bg-linear-to-r from-ttb05-acc to-orange-400 font-extrabold text-white shadow-[0_10px_24px_-10px_var(--color-ttb05-acc)] transition hover:-translate-y-0.5 hover:shadow-[0_14px_30px_-10px_var(--color-ttb05-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttb05-acc group-data-empty/cart:pointer-events-none group-data-empty/cart:opacity-40 motion-reduce:transition-none">Checkout <span aria-hidden="true">→</span></a>
        <p class="mt-3 text-center text-xs text-ttb05-mut">Taxes calculated at checkout · 30-day returns</p>
        <p role="status" class="sr-only"></p>
      </aside>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-ttb05-bg: #faf7f3;
  --color-ttb05-bg: oklch(0.98 0.007 70);
  --color-ttb05-line: #ece6de;
  --color-ttb05-line: oklch(0.93 0.012 70);
  --color-ttb05-ink: #1c1712;
  --color-ttb05-ink: oklch(0.2 0.015 60);
  --color-ttb05-mut: #6b6157;
  --color-ttb05-mut: oklch(0.5 0.02 60);
  --color-ttb05-acc: #f97316;
  --color-ttb05-acc: oklch(0.7 0.19 47);
  --color-ttb05-acc-ink: #b44a0c;
  --color-ttb05-acc-ink: oklch(0.53 0.15 45);
  --font-ttb05-sans: 'Manrope', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-05 {
  width: 100%;
  min-height: 100svh;
}
(() => {
  const root = document.getElementById('ttb05-cart');
  if (!root || root.dataset.wired) return;
  root.dataset.wired = '1';
  const FREE = 500, SHIP = 12;
  const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  const q = (s) => root.querySelector(s);
  const status = q('[role="status"]');
  const recalc = () => {
    const rows = Array.from(root.querySelectorAll('tbody tr[data-price]:not([data-removing])'));
    let sub = 0, items = 0;
    rows.forEach((r) => {
      const input = r.querySelector('input');
      const n = Math.max(1, Math.min(99, parseInt(input.value, 10) || 1));
      input.value = n;
      const line = n * Number(r.dataset.price);
      r.querySelector('[data-line]').textContent = fmt.format(line);
      r.querySelector('[data-step="-1"]').disabled = n <= 1;
      sub += line;
      items += n;
    });
    const ship = sub === 0 || sub >= FREE ? 0 : SHIP;
    q('[data-subtotal]').textContent = fmt.format(sub);
    q('[data-shipping]').textContent = ship ? fmt.format(ship) : 'Free';
    q('[data-total]').textContent = fmt.format(sub + ship);
    q('[data-count]').textContent = items + (items === 1 ? ' item' : ' items');
    q('[data-bar]').style.width = Math.min(100, sub / FREE * 100) + '%';
    q('[data-ship-msg]').textContent = sub >= FREE ? 'You’ve unlocked free express shipping' : 'Add ' + fmt.format(FREE - sub) + ' more for free express shipping';
    root.toggleAttribute('data-empty', rows.length === 0);
  };
  root.addEventListener('click', (e) => {
    const step = e.target.closest('[data-step]');
    if (step) {
      const input = step.closest('tr').querySelector('input');
      input.value = (parseInt(input.value, 10) || 1) + Number(step.dataset.step);
      recalc();
      return;
    }
    const rm = e.target.closest('[data-remove]');
    if (rm) {
      const tr = rm.closest('tr');
      tr.setAttribute('data-removing', '');
      status.textContent = tr.dataset.name + ' removed from cart';
      recalc();
      setTimeout(() => tr.remove(), 320);
    }
  });
  root.addEventListener('change', (e) => { if (e.target.matches('input[type="number"]')) recalc(); });
  recalc();
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-ttb05-bg: #faf7f3;
  --color-ttb05-bg: oklch(0.98 0.007 70);
  --color-ttb05-line: #ece6de;
  --color-ttb05-line: oklch(0.93 0.012 70);
  --color-ttb05-ink: #1c1712;
  --color-ttb05-ink: oklch(0.2 0.015 60);
  --color-ttb05-mut: #6b6157;
  --color-ttb05-mut: oklch(0.5 0.02 60);
  --color-ttb05-acc: #f97316;
  --color-ttb05-acc: oklch(0.7 0.19 47);
  --color-ttb05-acc-ink: #b44a0c;
  --color-ttb05-acc-ink: oklch(0.53 0.15 45);
  --font-ttb05-sans: 'Manrope', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-05 {
  width: 100%;
  min-height: 100svh;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400..800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • tr.ct-05--removed became a data-removing attribute read by data-removing:opacity-0 data-removing:translate-x-6 — the script no longer knows any class names.
  • The spinner-hiding pair (-moz-appearance:textfield + ::-webkit-inner-spin-button) became [appearance:textfield] and two arbitrary pseudo-element variants on the input.
  • UI upgrade from the CSS original: the steppers were decorative and totals were hard-coded — now every line, subtotal, shipping and total recalculates, the − button disables at 1, removed rows actually leave the layout, and an empty state replaces the table.
  • UI upgrade: a free-shipping progress bar, an order-summary sidebar that docks beside the table at @3xl, emoji thumbnails replaced with product monogram tiles, 44px controls with item-specific aria-labels, and the unit-price column hidden on phones.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Shopping Cart Table with Quantity Stepper & Live Totals
Source: https://codefronts.com/snippets/tailwind-css-tables/tailwind-shopping-cart-table-quantity-stepper-live-totals/

A light, warm checkout cart that actually works: steppers recalculate every line, the subtotal, shipping and total; removing an item swipes the row out and collapses it; and a free-shipping progress bar tells the shopper how far they are from the threshold. The summary moves beside the table at wide container sizes — the layout every Tier-1 storefront uses.
## HTML
```html
<section class="ttb-05 grid-cols-1 w-full min-h-svh grid place-items-center bg-ttb05-bg px-5 py-14 font-ttb05-sans text-ttb05-ink" aria-label="Shopping cart table demo">
  <div id="ttb05-cart" class="group/cart @container w-full max-w-5xl">
    <header class="mb-6 flex flex-wrap items-end justify-between gap-3">
      <div>
        <h2 class="text-2xl font-extrabold tracking-[-0.02em]">Your cart</h2>
        <p class="mt-1 text-sm text-ttb05-mut"><span data-count>4 items</span> · ships in 1–2 business days</p>
      </div>
      <a href="#" class="text-sm font-semibold text-ttb05-acc-ink underline decoration-ttb05-acc/40 underline-offset-4 hover:decoration-ttb05-acc">Continue shopping</a>
    </header>
    <div class="grid grid-cols-1 items-start gap-6 @3xl:grid-cols-[minmax(0,1fr)_20rem]">
      <div class="relative min-w-0 overflow-x-auto rounded-2xl bg-white shadow-[0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0.4_0.05_50/0.18)] ring-1 ring-ttb05-line">
        <table class="w-full border-collapse text-left text-sm group-data-empty/cart:hidden">
          <caption class="sr-only">Items in your shopping cart</caption>
          <thead class="[&_th]:border-b [&_th]:border-ttb05-line [&_th]:px-5 [&_th]:py-3.5 [&_th]:text-[11px] [&_th]:font-bold [&_th]:tracking-[0.12em] [&_th]:text-ttb05-mut [&_th]:uppercase">
            <tr><th scope="col">Product</th><th scope="col" class="text-right max-sm:hidden">Price</th><th scope="col" class="text-center">Qty</th><th scope="col" class="text-right">Total</th><th scope="col"><span class="sr-only">Remove</span></th></tr>
          </thead>
          <tbody class="[&_td]:px-5 [&_td]:py-5 [&_td]:align-middle max-sm:[&_td]:px-2.5 max-sm:[&_td:first-child]:pl-4">
            <tr data-price="249" data-name="ProSound ANC Headphones" class="border-b border-ttb05-line transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-amber-200 to-amber-400 text-sm font-extrabold text-amber-900/70 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">ANC</span><div><div class="font-bold">ProSound ANC Headphones</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Midnight black · PS-ANC-900</div><span class="mt-1.5 inline-block rounded bg-ttb05-acc/10 px-1.5 py-0.5 text-[11px] font-semibold text-ttb05-acc-ink">In stock</span></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$249.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of ProSound ANC Headphones" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="1" aria-label="Quantity of ProSound ANC Headphones" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of ProSound ANC Headphones" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$249.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove ProSound ANC Headphones" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
            <tr data-price="129" data-name="MechKey RGB Keyboard" class="border-b border-ttb05-line transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-indigo-200 to-indigo-400 text-sm font-extrabold text-indigo-950/60 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">TKL</span><div><div class="font-bold">MechKey RGB Keyboard</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Arctic white · MK-RGB-TKL</div><span class="mt-1.5 inline-block rounded bg-ttb05-acc/10 px-1.5 py-0.5 text-[11px] font-semibold text-ttb05-acc-ink">Best seller</span></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$129.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of MechKey RGB Keyboard" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="2" aria-label="Quantity of MechKey RGB Keyboard" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of MechKey RGB Keyboard" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$258.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove MechKey RGB Keyboard" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
            <tr data-price="79" data-name="ErgoClick Pro Mouse" class="transition duration-300 data-removing:translate-x-6 data-removing:opacity-0 motion-reduce:transition-none">
              <td><div class="group/p flex items-center gap-4"><span aria-hidden="true" class="grid size-16 max-sm:size-12 flex-none place-items-center rounded-xl bg-linear-135 from-emerald-200 to-emerald-400 text-sm font-extrabold text-emerald-950/60 ring-1 ring-black/5 transition group-hover/p:-rotate-3 group-hover/p:scale-105 motion-reduce:transition-none">G5</span><div><div class="font-bold">ErgoClick Pro Mouse</div><div class="mt-0.5 text-xs text-ttb05-mut max-sm:hidden">Graphite · EC-PRO-G502</div></div></div></td>
              <td class="text-right text-ttb05-mut tabular-nums max-sm:hidden">$79.00</td>
              <td><div class="mx-auto flex w-max items-center rounded-xl ring-1 ring-ttb05-line"><button type="button" data-step="-1" aria-label="Decrease quantity of ErgoClick Pro Mouse" class="grid h-11 w-10 place-items-center rounded-l-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white disabled:pointer-events-none disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-ttb05-acc">−</button><input type="number" min="1" max="99" value="1" aria-label="Quantity of ErgoClick Pro Mouse" class="h-11 w-10 bg-transparent text-center font-bold tabular-nums [appearance:textfield] focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"><button type="button" data-step="1" aria-label="Increase quantity of ErgoClick Pro Mouse" class="grid h-11 w-10 place-items-center rounded-r-xl text-lg font-bold text-ttb05-mut transition hover:bg-ttb05-acc hover:text-white focus-visible:outline-2 focus-visible:outline-ttb05-acc">+</button></div></td>
              <td data-line class="text-right text-[15px] font-extrabold tabular-nums">$79.00</td>
              <td class="w-0"><button type="button" data-remove aria-label="Remove ErgoClick Pro Mouse" class="grid size-11 place-items-center rounded-xl text-ttb05-mut transition hover:bg-red-50 hover:text-red-600 focus-visible:outline-2 focus-visible:outline-red-600"><svg class="size-4.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button></td>
            </tr>
          </tbody>
        </table>
        <p class="hidden px-6 py-16 text-center text-ttb05-mut group-data-empty/cart:block">Your cart is empty. <a href="#" class="font-semibold text-ttb05-acc-ink underline underline-offset-4">Browse best sellers</a></p>
      </div>
      <aside class="rounded-2xl bg-white p-6 shadow-[0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0.4_0.05_50/0.18)] ring-1 ring-ttb05-line" aria-label="Order summary">
        <h3 class="text-base font-extrabold">Order summary</h3>
        <div class="mt-4 rounded-xl bg-ttb05-bg p-3.5">
          <p data-ship-msg class="text-xs font-semibold text-ttb05-ink">You’ve unlocked free express shipping</p>
          <div class="mt-2 h-1.5 overflow-hidden rounded-full bg-ttb05-line"><div data-bar class="h-full w-full rounded-full bg-linear-to-r from-ttb05-acc to-amber-400 transition-[width] duration-500 motion-reduce:transition-none"></div></div>
        </div>
        <dl class="mt-5 grid gap-3 text-sm [&_dd]:text-right [&_dd]:font-semibold [&_dd]:tabular-nums [&_dt]:text-ttb05-mut">
          <div class="flex justify-between"><dt>Subtotal</dt><dd data-subtotal>$586.00</dd></div>
          <div class="flex justify-between"><dt>Shipping</dt><dd data-shipping class="text-emerald-700">Free</dd></div>
          <div class="flex justify-between border-t border-ttb05-line pt-3 text-base"><dt class="font-bold text-ttb05-ink!">Total</dt><dd data-total class="text-lg font-extrabold">$586.00</dd></div>
        </dl>
        <a href="#" class="mt-6 flex min-h-12 items-center justify-center gap-2 rounded-xl bg-linear-to-r from-ttb05-acc to-orange-400 font-extrabold text-white shadow-[0_10px_24px_-10px_var(--color-ttb05-acc)] transition hover:-translate-y-0.5 hover:shadow-[0_14px_30px_-10px_var(--color-ttb05-acc)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ttb05-acc group-data-empty/cart:pointer-events-none group-data-empty/cart:opacity-40 motion-reduce:transition-none">Checkout <span aria-hidden="true">→</span></a>
        <p class="mt-3 text-center text-xs text-ttb05-mut">Taxes calculated at checkout · 30-day returns</p>
        <p role="status" class="sr-only"></p>
      </aside>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-ttb05-bg: #faf7f3;
  --color-ttb05-bg: oklch(0.98 0.007 70);
  --color-ttb05-line: #ece6de;
  --color-ttb05-line: oklch(0.93 0.012 70);
  --color-ttb05-ink: #1c1712;
  --color-ttb05-ink: oklch(0.2 0.015 60);
  --color-ttb05-mut: #6b6157;
  --color-ttb05-mut: oklch(0.5 0.02 60);
  --color-ttb05-acc: #f97316;
  --color-ttb05-acc: oklch(0.7 0.19 47);
  --color-ttb05-acc-ink: #b44a0c;
  --color-ttb05-acc-ink: oklch(0.53 0.15 45);
  --font-ttb05-sans: 'Manrope', system-ui, sans-serif;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
   stage is sized before the browser build compiles. */

.ttb-05 {
  width: 100%;
  min-height: 100svh;
}
```

## JavaScript
```js
(() => {
  const root = document.getElementById('ttb05-cart');
  if (!root || root.dataset.wired) return;
  root.dataset.wired = '1';
  const FREE = 500, SHIP = 12;
  const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
  const q = (s) => root.querySelector(s);
  const status = q('[role="status"]');
  const recalc = () => {
    const rows = Array.from(root.querySelectorAll('tbody tr[data-price]:not([data-removing])'));
    let sub = 0, items = 0;
    rows.forEach((r) => {
      const input = r.querySelector('input');
      const n = Math.max(1, Math.min(99, parseInt(input.value, 10) || 1));
      input.value = n;
      const line = n * Number(r.dataset.price);
      r.querySelector('[data-line]').textContent = fmt.format(line);
      r.querySelector('[data-step="-1"]').disabled = n <= 1;
      sub += line;
      items += n;
    });
    const ship = sub === 0 || sub >= FREE ? 0 : SHIP;
    q('[data-subtotal]').textContent = fmt.format(sub);
    q('[data-shipping]').textContent = ship ? fmt.format(ship) : 'Free';
    q('[data-total]').textContent = fmt.format(sub + ship);
    q('[data-count]').textContent = items + (items === 1 ? ' item' : ' items');
    q('[data-bar]').style.width = Math.min(100, sub / FREE * 100) + '%';
    q('[data-ship-msg]').textContent = sub >= FREE ? 'You’ve unlocked free express shipping' : 'Add ' + fmt.format(FREE - sub) + ' more for free express shipping';
    root.toggleAttribute('data-empty', rows.length === 0);
  };
  root.addEventListener('click', (e) => {
    const step = e.target.closest('[data-step]');
    if (step) {
      const input = step.closest('tr').querySelector('input');
      input.value = (parseInt(input.value, 10) || 1) + Number(step.dataset.step);
      recalc();
      return;
    }
    const rm = e.target.closest('[data-remove]');
    if (rm) {
      const tr = rm.closest('tr');
      tr.setAttribute('data-removing', '');
      status.textContent = tr.dataset.name + ' removed from cart';
      recalc();
      setTimeout(() => tr.remove(), 320);
    }
  });
  root.addEventListener('change', (e) => { if (e.target.matches('input[type="number"]')) recalc(); });
  recalc();
})();
```

How this works

One delegated listener, attributes out. Clicks on [data-step] change the quantity; clicks on [data-remove] set data-removing on the row. Every recalculation formats with Intl.NumberFormat and writes text, a bar width and — when nothing is left — a data-empty attribute on the root:

<tr class="transition duration-300 data-removing:translate-x-6 data-removing:opacity-0">
<div id="ttb05-cart" class="group/cart"> … <p class="hidden group-data-empty/cart:block">

The row really leaves. The CSS original faded the row but left its space behind; here the row is removed from the DOM after the 300ms transition, and a role="status" region announces it.

Free shipping is a threshold, not a constant. Above $500 shipping is free and the bar is full; drop below and $12 is added, with copy that says exactly how much more to spend.

Make it yours

  • Threshold and fee: edit FREE and SHIP at the top of the script.
  • Currency: change the Intl.NumberFormat locale and currency, e.g. 'en-GB', { currency: 'GBP' }.
  • Real images: replace the monogram tile with <img class="size-16 rounded-xl object-cover">.
  • Max quantity: the clamp is Math.min(99, …) — set it from stock with a data-max attribute.

Gotchas — read before shipping

  • type="number" inputs accept typed values outside min/max — the script clamps on change, not only on button clicks.
  • Without JavaScript the steppers and remove buttons do nothing; in production wrap each row in a form that posts to your cart endpoint.
  • translate on a <tr> works in all evergreen engines, but borders on collapsed tables don't move with it — rows use their own border-b.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Data-attribute variants and container queries sit under Tailwind v4's floor; Intl.NumberFormat is universal.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…