Tailwind + JSMIT licensed

Tailwind Food Delivery Card with Quantity Stepper

The delivery-app dish card: dietary pills, a rating and prep-time line, and a +/− stepper driving a live running total. CSS owns all of it except two numbers — the script writes the count and the total, sets [disabled] so disabled: can style it, and announces the change in an aria-live region.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tpc-16 w-full min-h-svh grid place-items-center bg-tpc16-bg font-tpc16-sans px-5 py-12" data-price="12.5" aria-label="Food delivery dish card with quantity stepper demo">
  <article class="w-[min(360px,100%)] rounded-[18px] overflow-hidden bg-tpc16-card shadow-[0_16px_40px_-22px_oklch(0.3_0.04_60/0.4)]">
    <div role="img" aria-label="Dish photo: Smoky bean chilli bowl" class="aspect-[16/10] bg-linear-[135deg,var(--color-tpc16-ph),var(--color-tpc16-ph2)] bg-[url('https://images.unsplash.com/photo-1547592166-23ac45744acd?w=800&h=500&fit=crop&q=70')] bg-cover bg-center"></div>
    <div class="px-5 pt-4.5 pb-5">
      <div class="flex items-start justify-between gap-3">
        <h3 class="text-[18px] font-bold text-tpc16-ink text-pretty">Smoky Bean Chilli Bowl</h3>
        <div class="flex gap-1.5 shrink-0">
          <span class="px-2.25 py-1 rounded-full bg-tpc16-veg text-tpc16-vegink text-[10px] font-bold tracking-[0.04em] uppercase">Vegan</span>
          <span class="px-2.25 py-1 rounded-full bg-tpc16-gf text-tpc16-gfink text-[10px] font-bold tracking-[0.04em] uppercase">GF</span>
        </div>
      </div>
      <p class="mt-2.5 text-[13.5px] leading-[1.5] text-tpc16-mut">Slow-cooked black beans, fire-roasted peppers, lime crema.</p>
      <p class="mt-2.5 text-[13px] text-tpc16-mut2"><span aria-hidden="true" class="text-tpc16-star">&#9733;</span> 4.7 &middot; 20&ndash;30 min &middot; $2.99 delivery</p>
      <div class="mt-4.5 flex items-center justify-between gap-3">
        <div role="group" aria-label="Quantity" class="inline-flex items-center border border-tpc16-edge rounded-xl overflow-hidden">
          <button type="button" data-dec aria-label="Decrease quantity" class="grid place-items-center min-w-11 min-h-11 border-0 cursor-pointer bg-tpc16-step text-tpc16-ink text-[20px] font-semibold leading-none transition-colors duration-150 hover:bg-tpc16-step2 disabled:opacity-35 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-tpc16-acc focus-visible:-outline-offset-2 motion-reduce:transition-none">&minus;</button>
          <span data-qty class="min-w-9 text-center text-[15px] font-bold text-tpc16-ink tabular-nums">1</span>
          <button type="button" data-inc aria-label="Increase quantity" class="grid place-items-center min-w-11 min-h-11 border-0 cursor-pointer bg-tpc16-step text-tpc16-ink text-[20px] font-semibold leading-none transition-colors duration-150 hover:bg-tpc16-step2 disabled:opacity-35 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-tpc16-acc focus-visible:-outline-offset-2 motion-reduce:transition-none">&plus;</button>
        </div>
        <button type="button" data-add aria-live="polite" class="flex-1 min-h-11 h-11.5 rounded-xl border-0 cursor-pointer bg-tpc16-acc text-tpc16-onacc text-[14px] font-bold tabular-nums transition-[filter] duration-150 hover:brightness-108 focus-visible:outline-3 focus-visible:outline-tpc16-ring focus-visible:outline-offset-2 motion-reduce:transition-none">Add &middot; $12.50</button>
      </div>
    </div>
  </article>
</section>
@import "tailwindcss";

@theme {
  --color-tpc16-bg: #f6f3ee;
  --color-tpc16-bg: oklch(0.96 0.008 75);
  --color-tpc16-card: #ffffff;
  --color-tpc16-card: oklch(1 0 0);
  --color-tpc16-ink: #2a1e12;
  --color-tpc16-ink: oklch(0.26 0.025 60);
  --color-tpc16-mut: #5f5344;
  --color-tpc16-mut: oklch(0.46 0.02 65);
  --color-tpc16-mut2: #6d6152;
  --color-tpc16-mut2: oklch(0.51 0.02 68);
  --color-tpc16-edge: #e4dccf;
  --color-tpc16-edge: oklch(0.9 0.015 80);
  --color-tpc16-step: #faf7f2;
  --color-tpc16-step: oklch(0.98 0.006 80);
  --color-tpc16-step2: #f0e8db;
  --color-tpc16-step2: oklch(0.94 0.014 80);
  --color-tpc16-ph: #f0e4d4;
  --color-tpc16-ph: oklch(0.93 0.02 75);
  --color-tpc16-ph2: #e6d2b8;
  --color-tpc16-ph2: oklch(0.88 0.035 78);
  --color-tpc16-veg: #dcf2e0;
  --color-tpc16-veg: oklch(0.93 0.06 150);
  --color-tpc16-vegink: #1f6b3b;
  --color-tpc16-vegink: oklch(0.42 0.12 150);
  --color-tpc16-gf: #dde9f8;
  --color-tpc16-gf: oklch(0.93 0.05 250);
  --color-tpc16-gfink: #1f548f;
  --color-tpc16-gfink: oklch(0.44 0.12 255);
  --color-tpc16-star: #c78a12;
  --color-tpc16-star: oklch(0.68 0.14 75);
  --color-tpc16-acc: #d4601f;
  --color-tpc16-acc: oklch(0.6 0.17 45);
  --color-tpc16-ring: #f0b48c;
  --color-tpc16-ring: oklch(0.82 0.09 45);
  --color-tpc16-onacc: #ffffff;
  --color-tpc16-onacc: oklch(1 0 0);
  --font-tpc16-sans: 'Outfit', system-ui, sans-serif;
}

.tpc-16 {
  width: 100%;
  min-height: 100svh;
}

.tpc-16 ::selection {
  background: var(--color-tpc16-acc);
  color: #fff;
}
document.querySelectorAll('.tpc-16').forEach(function (root) {
  if (root.dataset.bound) return;
  root.dataset.bound = '1';

  var unit = parseFloat(root.dataset.price) || 0;
  var qtyEl = root.querySelector('[data-qty]');
  var addEl = root.querySelector('[data-add]');
  var inc = root.querySelector('[data-inc]');
  var dec = root.querySelector('[data-dec]');
  if (!qtyEl || !addEl || !inc || !dec) return;

  var qty = 1;
  var MAX = 20;

  function render() {
    qtyEl.textContent = qty;
    addEl.textContent = 'Add \u00b7 $' + (unit * qty).toFixed(2);
    dec.disabled = qty <= 1;      // JS sets the attribute; disabled: styles it
    inc.disabled = qty >= MAX;
  }

  inc.addEventListener('click', function () { if (qty < MAX) { qty++; render(); } });
  dec.addEventListener('click', function () { if (qty > 1) { qty--; render(); } });
  render();
});

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-tpc16-bg: #f6f3ee;
  --color-tpc16-bg: oklch(0.96 0.008 75);
  --color-tpc16-card: #ffffff;
  --color-tpc16-card: oklch(1 0 0);
  --color-tpc16-ink: #2a1e12;
  --color-tpc16-ink: oklch(0.26 0.025 60);
  --color-tpc16-mut: #5f5344;
  --color-tpc16-mut: oklch(0.46 0.02 65);
  --color-tpc16-mut2: #6d6152;
  --color-tpc16-mut2: oklch(0.51 0.02 68);
  --color-tpc16-edge: #e4dccf;
  --color-tpc16-edge: oklch(0.9 0.015 80);
  --color-tpc16-step: #faf7f2;
  --color-tpc16-step: oklch(0.98 0.006 80);
  --color-tpc16-step2: #f0e8db;
  --color-tpc16-step2: oklch(0.94 0.014 80);
  --color-tpc16-ph: #f0e4d4;
  --color-tpc16-ph: oklch(0.93 0.02 75);
  --color-tpc16-ph2: #e6d2b8;
  --color-tpc16-ph2: oklch(0.88 0.035 78);
  --color-tpc16-veg: #dcf2e0;
  --color-tpc16-veg: oklch(0.93 0.06 150);
  --color-tpc16-vegink: #1f6b3b;
  --color-tpc16-vegink: oklch(0.42 0.12 150);
  --color-tpc16-gf: #dde9f8;
  --color-tpc16-gf: oklch(0.93 0.05 250);
  --color-tpc16-gfink: #1f548f;
  --color-tpc16-gfink: oklch(0.44 0.12 255);
  --color-tpc16-star: #c78a12;
  --color-tpc16-star: oklch(0.68 0.14 75);
  --color-tpc16-acc: #d4601f;
  --color-tpc16-acc: oklch(0.6 0.17 45);
  --color-tpc16-ring: #f0b48c;
  --color-tpc16-ring: oklch(0.82 0.09 45);
  --color-tpc16-onacc: #ffffff;
  --color-tpc16-onacc: oklch(1 0 0);
  --font-tpc16-sans: 'Outfit', system-ui, sans-serif;
}

.tpc-16 {
  width: 100%;
  min-height: 100svh;
}

.tpc-16 ::selection {
  background: var(--color-tpc16-acc);
  color: #fff;
}
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=Outfit:wght@400;500;600;700;800&display=swap">

What changed in the port

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

  • The cleanest demonstration in the collection of where the line falls: the script writes two text nodes and one boolean attribute, and disabled:, hover: and focus-visible: handle every appearance those states imply. No classList.add anywhere.
  • getElementById and five hard-coded ids become querySelectorAll('.tpc-16') plus [data-qty]-style hooks, so the snippet works pasted anywhere and drives every card on a menu page, not just the first.
  • focus-visible:-outline-offset-2 — the negative offset is load-bearing. The stepper shell is overflow-hidden to get its seamless seam, which would clip a normal outset ring.
  • Enhancement over the CSS original: the increment button now disables at the maximum instead of silently ignoring presses, the stepper controls grew to 44px, muted text was darkened to clear 4.5:1, and both the count and the total are tabular-nums so the row stops jogging as the price crosses a digit.
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 Food Delivery Card with Quantity Stepper
Source: https://codefronts.com/components/tailwind-css-product-cards/tailwind-food-delivery-card-quantity-stepper/

The delivery-app dish card: dietary pills, a rating and prep-time line, and a &plus;/&minus; stepper driving a live running total. CSS owns all of it except two numbers — the script writes the count and the total, sets [disabled] so disabled: can style it, and announces the change in an aria-live region.
## HTML
```html
<section class="tpc-16 w-full min-h-svh grid place-items-center bg-tpc16-bg font-tpc16-sans px-5 py-12" data-price="12.5" aria-label="Food delivery dish card with quantity stepper demo">
  <article class="w-[min(360px,100%)] rounded-[18px] overflow-hidden bg-tpc16-card shadow-[0_16px_40px_-22px_oklch(0.3_0.04_60/0.4)]">
    <div role="img" aria-label="Dish photo: Smoky bean chilli bowl" class="aspect-[16/10] bg-linear-[135deg,var(--color-tpc16-ph),var(--color-tpc16-ph2)] bg-[url('https://images.unsplash.com/photo-1547592166-23ac45744acd?w=800&h=500&fit=crop&q=70')] bg-cover bg-center"></div>
    <div class="px-5 pt-4.5 pb-5">
      <div class="flex items-start justify-between gap-3">
        <h3 class="text-[18px] font-bold text-tpc16-ink text-pretty">Smoky Bean Chilli Bowl</h3>
        <div class="flex gap-1.5 shrink-0">
          <span class="px-2.25 py-1 rounded-full bg-tpc16-veg text-tpc16-vegink text-[10px] font-bold tracking-[0.04em] uppercase">Vegan</span>
          <span class="px-2.25 py-1 rounded-full bg-tpc16-gf text-tpc16-gfink text-[10px] font-bold tracking-[0.04em] uppercase">GF</span>
        </div>
      </div>
      <p class="mt-2.5 text-[13.5px] leading-[1.5] text-tpc16-mut">Slow-cooked black beans, fire-roasted peppers, lime crema.</p>
      <p class="mt-2.5 text-[13px] text-tpc16-mut2"><span aria-hidden="true" class="text-tpc16-star">&#9733;</span> 4.7 &middot; 20&ndash;30 min &middot; $2.99 delivery</p>
      <div class="mt-4.5 flex items-center justify-between gap-3">
        <div role="group" aria-label="Quantity" class="inline-flex items-center border border-tpc16-edge rounded-xl overflow-hidden">
          <button type="button" data-dec aria-label="Decrease quantity" class="grid place-items-center min-w-11 min-h-11 border-0 cursor-pointer bg-tpc16-step text-tpc16-ink text-[20px] font-semibold leading-none transition-colors duration-150 hover:bg-tpc16-step2 disabled:opacity-35 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-tpc16-acc focus-visible:-outline-offset-2 motion-reduce:transition-none">&minus;</button>
          <span data-qty class="min-w-9 text-center text-[15px] font-bold text-tpc16-ink tabular-nums">1</span>
          <button type="button" data-inc aria-label="Increase quantity" class="grid place-items-center min-w-11 min-h-11 border-0 cursor-pointer bg-tpc16-step text-tpc16-ink text-[20px] font-semibold leading-none transition-colors duration-150 hover:bg-tpc16-step2 disabled:opacity-35 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-tpc16-acc focus-visible:-outline-offset-2 motion-reduce:transition-none">&plus;</button>
        </div>
        <button type="button" data-add aria-live="polite" class="flex-1 min-h-11 h-11.5 rounded-xl border-0 cursor-pointer bg-tpc16-acc text-tpc16-onacc text-[14px] font-bold tabular-nums transition-[filter] duration-150 hover:brightness-108 focus-visible:outline-3 focus-visible:outline-tpc16-ring focus-visible:outline-offset-2 motion-reduce:transition-none">Add &middot; $12.50</button>
      </div>
    </div>
  </article>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tpc16-bg: #f6f3ee;
  --color-tpc16-bg: oklch(0.96 0.008 75);
  --color-tpc16-card: #ffffff;
  --color-tpc16-card: oklch(1 0 0);
  --color-tpc16-ink: #2a1e12;
  --color-tpc16-ink: oklch(0.26 0.025 60);
  --color-tpc16-mut: #5f5344;
  --color-tpc16-mut: oklch(0.46 0.02 65);
  --color-tpc16-mut2: #6d6152;
  --color-tpc16-mut2: oklch(0.51 0.02 68);
  --color-tpc16-edge: #e4dccf;
  --color-tpc16-edge: oklch(0.9 0.015 80);
  --color-tpc16-step: #faf7f2;
  --color-tpc16-step: oklch(0.98 0.006 80);
  --color-tpc16-step2: #f0e8db;
  --color-tpc16-step2: oklch(0.94 0.014 80);
  --color-tpc16-ph: #f0e4d4;
  --color-tpc16-ph: oklch(0.93 0.02 75);
  --color-tpc16-ph2: #e6d2b8;
  --color-tpc16-ph2: oklch(0.88 0.035 78);
  --color-tpc16-veg: #dcf2e0;
  --color-tpc16-veg: oklch(0.93 0.06 150);
  --color-tpc16-vegink: #1f6b3b;
  --color-tpc16-vegink: oklch(0.42 0.12 150);
  --color-tpc16-gf: #dde9f8;
  --color-tpc16-gf: oklch(0.93 0.05 250);
  --color-tpc16-gfink: #1f548f;
  --color-tpc16-gfink: oklch(0.44 0.12 255);
  --color-tpc16-star: #c78a12;
  --color-tpc16-star: oklch(0.68 0.14 75);
  --color-tpc16-acc: #d4601f;
  --color-tpc16-acc: oklch(0.6 0.17 45);
  --color-tpc16-ring: #f0b48c;
  --color-tpc16-ring: oklch(0.82 0.09 45);
  --color-tpc16-onacc: #ffffff;
  --color-tpc16-onacc: oklch(1 0 0);
  --font-tpc16-sans: 'Outfit', system-ui, sans-serif;
}

.tpc-16 {
  width: 100%;
  min-height: 100svh;
}

.tpc-16 ::selection {
  background: var(--color-tpc16-acc);
  color: #fff;
}
```

## JavaScript
```js
document.querySelectorAll('.tpc-16').forEach(function (root) {
  if (root.dataset.bound) return;
  root.dataset.bound = '1';

  var unit = parseFloat(root.dataset.price) || 0;
  var qtyEl = root.querySelector('[data-qty]');
  var addEl = root.querySelector('[data-add]');
  var inc = root.querySelector('[data-inc]');
  var dec = root.querySelector('[data-dec]');
  if (!qtyEl || !addEl || !inc || !dec) return;

  var qty = 1;
  var MAX = 20;

  function render() {
    qtyEl.textContent = qty;
    addEl.textContent = 'Add \u00b7 $' + (unit * qty).toFixed(2);
    dec.disabled = qty <= 1;      // JS sets the attribute; disabled: styles it
    inc.disabled = qty >= MAX;
  }

  inc.addEventListener('click', function () { if (qty < MAX) { qty++; render(); } });
  dec.addEventListener('click', function () { if (qty > 1) { qty--; render(); } });
  render();
});
```

How this works

The split is the point. Every visual state is a utility — hover, focus, disabled, the pills, the stepper's seam. The script owns two strings:

qtyEl.textContent = qty;
addEl.textContent = 'Add · $' + (unit * qty).toFixed(2);
dec.disabled = qty <= 1;

That last line is the interesting one: JS sets the attribute, and disabled:opacity-35 decides what it looks like. Styling stays in the markup even for a state only JavaScript can reach.

Price lives in the DOM, not the script. data-price="12.5" on the root means the same eight lines drive any dish on the menu, and the controller starts from querySelectorAll('.tpc-16') so a page of fifty cards needs one copy of it.

Announcing a changing number. The button label is the total, and it carries aria-live="polite" — so 'Add · $37.50' is read after the third press without moving focus away from the stepper.

Make it yours

  • Unit price: the root's data-price. The script never hard-codes it.
  • Max quantity: the qty < 20 guard. Set it from a data-max for stock limits.
  • Dietary pills: add Halal, Spicy or Nut-free by copying one pill and giving it a colour pair.
  • Swap the total for an 'Add N to order' label if the count matters more than the money.
  • Retheme through --color-tpc16-acc; the add button, both focus rings and the selection colour read it.

Gotchas — read before shipping

  • The total must live in an aria-live="polite" region. Without it, a screen-reader user presses &plus; four times and hears nothing change.
  • Stepper controls must be real <button type="button"> with aria-label ('Increase quantity'), not clickable spans — and inside a role="group" with a label naming what is being stepped.
  • Guard the decrement at 1 in the script and reflect it with [disabled]; a visually dimmed button that still fires is worse than no guard.
  • The inset focus ring is deliberate. overflow-hidden on the stepper shell would clip a normal outset outline to nothing.
  • Without JS the card shows a static quantity of 1 and a correct single-unit price — check that your fallback is the honest number, not a zero.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The layout is universally supported; a small vanilla script drives the stepper and the live total. With JS disabled the card degrades to a static quantity of one at the correct price.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…