Open demo

CSS code

HTML
<div><span class="lbl">Grocery cards with a quantity stepper</span>
<ul class="gc">
  <li><article class="gc-card">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?auto=format&fit=crop&w=400&q=70" alt="Halved avocado with its stone on a pink background" width="400" height="400" loading="lazy"><span class="gc-tag">Organic</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#">Hass Avocados</a></h3>
      <p class="gc-unit">Ripe and ready · each</p>
      <p class="gc-price">$1.25 <small>/ each</small></p>
      <div class="gc-foot">
        <div class="gc-stepper" role="group" aria-label="Quantity of Hass Avocados">
          <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
          <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Hass Avocados to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="gc-card">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?auto=format&fit=crop&w=400&q=70" alt="Single red apple on a light grey background" width="400" height="400" loading="lazy"><span class="gc-tag is-deal">2 for $6</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#">Honeycrisp Apples</a></h3>
      <p class="gc-unit">1 kg bag · $3.40/kg</p>
      <p class="gc-price">$3.40 <small>/ bag</small></p>
      <div class="gc-foot">
        <div class="gc-stepper" role="group" aria-label="Quantity of Honeycrisp Apples">
          <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Honeycrisp Apples to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="gc-card">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?auto=format&fit=crop&w=400&q=70" alt="Bunch of ripe yellow bananas on a yellow background" width="400" height="400" loading="lazy"><span class="gc-tag">Fair trade</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#">Bananas</a></h3>
      <p class="gc-unit">Bunch of 5–6 · about 1 kg</p>
      <p class="gc-price">$1.89 <small>/ bunch</small></p>
      <div class="gc-foot">
        <div class="gc-stepper" role="group" aria-label="Quantity of Bananas">
          <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Bananas to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="gc-card">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1582722872445-44dc5f7e3c8f?auto=format&fit=crop&w=400&q=70" alt="Bowl of brown eggs held in two hands" width="400" height="400" loading="lazy"><span class="gc-tag">Free range</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#">Large Brown Eggs</a></h3>
      <p class="gc-unit">Box of 12 · $0.43 each</p>
      <p class="gc-price">$5.20 <small>/ box</small></p>
      <div class="gc-foot">
        <div class="gc-stepper" role="group" aria-label="Quantity of Large Brown Eggs">
          <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
          <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Large Brown Eggs to cart</span></button>
      </div>
    </div>
  </article></li>
</ul></div>

<div><span class="lbl">Row tiles for a basket or a quick-order list</span>
<ul class="gc-tiles">
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=70" alt="Round crusty loaves of bread with ears of wheat" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#">Sourdough Country Loaf</a></h3><p class="gc-unit">800 g · baked this morning</p><p class="gc-price">$6.50</p></div>
    <div class="gc-tile-actions">
      <div class="gc-stepper" role="group" aria-label="Quantity of Sourdough Country Loaf">
        <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="gc-add">Add<span class="sr-only"> Sourdough Country Loaf to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1592924357228-91a4daadcfea?auto=format&fit=crop&w=200&q=70" alt="Red vine tomatoes covered in water droplets on a dark background" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#">Vine Tomatoes</a></h3><p class="gc-unit">500 g · $5.98/kg</p><p class="gc-price">$2.99</p></div>
    <div class="gc-tile-actions">
      <div class="gc-stepper" role="group" aria-label="Quantity of Vine Tomatoes">
        <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
        <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="gc-add">Add<span class="sr-only"> Vine Tomatoes to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1559181567-c3190ca9959b?auto=format&fit=crop&w=200&q=70" alt="Pair of red cherries on a mint green background" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#">Sweet Cherries</a></h3><p class="gc-unit">400 g punnet · in season</p><p class="gc-price">$4.75</p></div>
    <div class="gc-tile-actions">
      <div class="gc-stepper" role="group" aria-label="Quantity of Sweet Cherries">
        <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
        <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="gc-add">Add<span class="sr-only"> Sweet Cherries to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1518977676601-b53f82aba655?auto=format&fit=crop&w=200&q=70" alt="Pile of unwashed golden potatoes" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#">Golden Potatoes</a></h3><p class="gc-unit">2 kg bag · $1.20/kg</p><p class="gc-price">$2.40</p></div>
    <div class="gc-tile-actions">
      <div class="gc-stepper" role="group" aria-label="Quantity of Golden Potatoes">
        <button type="button" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="gc-add">Add<span class="sr-only"> Golden Potatoes to cart</span></button>
    </div>
  </li>
</ul></div>
CSS
/* Grocery cards: price per unit, an organic or deal tag, and a quantity stepper (a labelled number input
   between two buttons). The input works on its own; the script only makes − and + step it within min/max. */
.gc,.gc-tiles{--gc-ink:#1a2e1f;--gc-muted:#4b5d50;--gc-line:#dfe7da;--gc-media:#eef3ea;--gc-green:#15803d;--gc-green-dark:#116632;--gc-tag:#dcfce7;--gc-tag-ink:#14532d;--gc-deal:#fef3c7;--gc-deal-ink:#78350f;--gc-focus:#2563eb}
.gc{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:18px;list-style:none;margin:0;padding:0}
.gc-card{display:flex;flex-direction:column;height:100%;padding:12px;background:#fff;border:1px solid var(--gc-line);border-radius:20px}
.gc-media{position:relative;aspect-ratio:1;border-radius:14px;background:var(--gc-media);overflow:hidden}
.gc-media img{display:block;width:100%;height:100%;object-fit:cover}
.gc-tag{position:absolute;top:8px;left:8px;padding:2px 9px;border-radius:999px;background:var(--gc-tag);color:var(--gc-tag-ink);font-size:.7rem;font-weight:700;letter-spacing:.02em}
.gc-tag.is-deal{background:var(--gc-deal);color:var(--gc-deal-ink)}
.gc-body{display:flex;flex:1;flex-direction:column;gap:2px;padding:12px 4px 0}
.gc-title{margin:0;font-size:.95rem;font-weight:600;line-height:1.3}
.gc-title a{color:var(--gc-ink);text-decoration:none}
.gc-title a:hover{text-decoration:underline;text-underline-offset:3px}
.gc-unit{margin:0;font-size:.78rem;color:var(--gc-muted)}
.gc-price{margin:6px 0 12px;font-size:1.15rem;font-weight:800;color:var(--gc-ink)}
.gc-price small{font-size:.75rem;font-weight:500;color:var(--gc-muted)}
.gc-foot{display:flex;align-items:center;gap:8px;margin-top:auto}
.gc-stepper{display:inline-flex;align-items:center;height:40px;border:1.5px solid #8fa596;border-radius:999px;background:#fff}
.gc-stepper button{display:grid;place-items:center;width:34px;height:100%;padding:0;border:0;border-radius:999px;background:none;color:var(--gc-ink);cursor:pointer}
.gc-stepper button:hover{background:var(--gc-media)}
.gc-stepper svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round}
.gc-stepper input{width:30px;height:100%;padding:0;border:0;background:none;color:var(--gc-ink);font:inherit;font-size:.92rem;font-weight:700;text-align:center;-moz-appearance:textfield;appearance:textfield}
.gc-stepper input::-webkit-inner-spin-button,.gc-stepper input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.gc-add{display:flex;flex:1;align-items:center;justify-content:center;gap:6px;height:40px;padding:0 12px;border:0;border-radius:999px;background:var(--gc-green);color:#fff;
  font:inherit;font-size:.85rem;font-weight:700;cursor:pointer;transition:background-color .15s}
.gc-add:hover{background:var(--gc-green-dark)}
.gc-add svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
/* Row tiles */
.gc-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:14px;list-style:none;margin:0;padding:0}
.gc-tile{display:grid;grid-template-columns:84px 1fr auto;align-items:center;gap:14px;padding:12px;background:#fff;border:1px solid var(--gc-line);border-radius:18px}
.gc-tile img{display:block;width:84px;height:84px;border-radius:12px;object-fit:cover;background:var(--gc-media)}
.gc-tile .gc-price{margin:4px 0 0;font-size:1.05rem}
.gc-tile-actions{display:flex;flex-direction:column;align-items:stretch;gap:6px}
.gc-tile .gc-add{flex:none;height:34px}
@media (max-width:480px){.gc-tiles{grid-template-columns:1fr}.gc-tile{grid-template-columns:72px 1fr}.gc-tile img{width:72px;height:72px}
  .gc-tile-actions{grid-column:1 / -1;flex-direction:row}.gc-tile .gc-add{flex:1;height:40px}}
:is(.gc,.gc-tiles) :is(a,button):focus-visible{outline:3px solid var(--gc-focus);outline-offset:2px}
.gc-stepper:focus-within{border-color:var(--gc-ink)}
.gc-stepper input:focus-visible{outline:3px solid var(--gc-focus);outline-offset:0;border-radius:6px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (prefers-reduced-motion:reduce){.gc-add{transition:none}}
JavaScript
// Quantity steppers: − and + change the number input by one, kept within its min and max.
document.querySelectorAll('[data-step]').forEach(btn => btn.addEventListener('click', () => {
  const input = btn.parentElement.querySelector('input');
  const min = Number(input.min) || 1, max = Number(input.max) || 99;
  input.value = Math.min(max, Math.max(min, (Number(input.value) || min) + Number(btn.dataset.step)));
  input.dispatchEvent(new Event('change', { bubbles: true }));
}));

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Grocery cards with a quantity stepper</span>
<ul class="gc row row-cols-1 row-cols-sm-2 row-cols-md-4 list-unstyled mb-0">
  <li class="col"><article class="card h-100">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?auto=format&fit=crop&w=400&q=70" alt="Halved avocado with its stone on a pink background" width="400" height="400" loading="lazy"><span class="gc-tag badge rounded-pill">Organic</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#" class="text-decoration-none">Hass Avocados</a></h3>
      <p class="gc-unit">Ripe and ready · each</p>
      <p class="gc-price">$1.25 <small>/ each</small></p>
      <div class="gc-foot">
        <div class="input-group gc-stepper" role="group" aria-label="Quantity of Hass Avocados">
          <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" class="form-control" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
          <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="btn gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="visually-hidden"> Hass Avocados to cart</span></button>
      </div>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?auto=format&fit=crop&w=400&q=70" alt="Single red apple on a light grey background" width="400" height="400" loading="lazy"><span class="gc-tag is-deal badge rounded-pill">2 for $6</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#" class="text-decoration-none">Honeycrisp Apples</a></h3>
      <p class="gc-unit">1 kg bag · $3.40/kg</p>
      <p class="gc-price">$3.40 <small>/ bag</small></p>
      <div class="gc-foot">
        <div class="input-group gc-stepper" role="group" aria-label="Quantity of Honeycrisp Apples">
          <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="btn gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="visually-hidden"> Honeycrisp Apples to cart</span></button>
      </div>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?auto=format&fit=crop&w=400&q=70" alt="Bunch of ripe yellow bananas on a yellow background" width="400" height="400" loading="lazy"><span class="gc-tag badge rounded-pill">Fair trade</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#" class="text-decoration-none">Bananas</a></h3>
      <p class="gc-unit">Bunch of 5–6 · about 1 kg</p>
      <p class="gc-price">$1.89 <small>/ bunch</small></p>
      <div class="gc-foot">
        <div class="input-group gc-stepper" role="group" aria-label="Quantity of Bananas">
          <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="btn gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="visually-hidden"> Bananas to cart</span></button>
      </div>
    </div>
  </article></li>
  <li class="col"><article class="card h-100">
    <div class="gc-media"><img src="https://images.unsplash.com/photo-1582722872445-44dc5f7e3c8f?auto=format&fit=crop&w=400&q=70" alt="Bowl of brown eggs held in two hands" width="400" height="400" loading="lazy"><span class="gc-tag badge rounded-pill">Free range</span></div>
    <div class="gc-body">
      <h3 class="gc-title"><a href="#" class="text-decoration-none">Large Brown Eggs</a></h3>
      <p class="gc-unit">Box of 12 · $0.43 each</p>
      <p class="gc-price">$5.20 <small>/ box</small></p>
      <div class="gc-foot">
        <div class="input-group gc-stepper" role="group" aria-label="Quantity of Large Brown Eggs">
          <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
          <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="btn gc-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="visually-hidden"> Large Brown Eggs to cart</span></button>
      </div>
    </div>
  </article></li>
</ul></div>

<div><span class="lbl">Row tiles for a basket or a quick-order list</span>
<ul class="gc-tiles list-unstyled mb-0">
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=70" alt="Round crusty loaves of bread with ears of wheat" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#" class="text-decoration-none">Sourdough Country Loaf</a></h3><p class="gc-unit">800 g · baked this morning</p><p class="gc-price">$6.50</p></div>
    <div class="gc-tile-actions">
      <div class="input-group gc-stepper" role="group" aria-label="Quantity of Sourdough Country Loaf">
        <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="btn gc-add">Add<span class="visually-hidden"> Sourdough Country Loaf to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1592924357228-91a4daadcfea?auto=format&fit=crop&w=200&q=70" alt="Red vine tomatoes covered in water droplets on a dark background" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#" class="text-decoration-none">Vine Tomatoes</a></h3><p class="gc-unit">500 g · $5.98/kg</p><p class="gc-price">$2.99</p></div>
    <div class="gc-tile-actions">
      <div class="input-group gc-stepper" role="group" aria-label="Quantity of Vine Tomatoes">
        <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" class="form-control" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
        <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="btn gc-add">Add<span class="visually-hidden"> Vine Tomatoes to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1559181567-c3190ca9959b?auto=format&fit=crop&w=200&q=70" alt="Pair of red cherries on a mint green background" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#" class="text-decoration-none">Sweet Cherries</a></h3><p class="gc-unit">400 g punnet · in season</p><p class="gc-price">$4.75</p></div>
    <div class="gc-tile-actions">
      <div class="input-group gc-stepper" role="group" aria-label="Quantity of Sweet Cherries">
        <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
        <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="btn gc-add">Add<span class="visually-hidden"> Sweet Cherries to cart</span></button>
    </div>
  </li>
  <li class="gc-tile">
    <img src="https://images.unsplash.com/photo-1518977676601-b53f82aba655?auto=format&fit=crop&w=200&q=70" alt="Pile of unwashed golden potatoes" width="84" height="84" loading="lazy">
    <div><h3 class="gc-title"><a href="#" class="text-decoration-none">Golden Potatoes</a></h3><p class="gc-unit">2 kg bag · $1.20/kg</p><p class="gc-price">$2.40</p></div>
    <div class="gc-tile-actions">
      <div class="input-group gc-stepper" role="group" aria-label="Quantity of Golden Potatoes">
        <button type="button" class="btn" data-step="-1" aria-label="Decrease quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input type="number" class="form-control" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" class="btn" data-step="1" aria-label="Increase quantity"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="btn gc-add">Add<span class="visually-hidden"> Golden Potatoes to cart</span></button>
    </div>
  </li>
</ul></div>
CSS
/* Grocery cards on Bootstrap: .card in a .row-cols-* grid, .badge tags, and an .input-group quantity stepper
   (a labelled .form-control number input between two .btn buttons). The script only makes − and + step it. */
.gc,.gc-tiles{--gc-ink:#1a2e1f;--gc-muted:#4b5d50;--gc-line:#dfe7da;--gc-media:#eef3ea;--gc-green:#15803d;--gc-green-dark:#116632;--gc-tag:#dcfce7;--gc-tag-ink:#14532d;--gc-deal:#fef3c7;--gc-deal-ink:#78350f;--gc-focus:#2563eb}
.gc{--bs-gutter-x:18px;--bs-gutter-y:18px}
.gc .card{--bs-card-border-color:var(--gc-line);--bs-card-border-radius:20px;padding:12px}
.gc-media{position:relative;aspect-ratio:1;border-radius:14px;background:var(--gc-media);overflow:hidden}
.gc-media img{display:block;width:100%;height:100%;object-fit:cover}
.gc-tag{position:absolute;top:8px;left:8px;padding:2px 9px;line-height:1.5;border-radius:999px;background:var(--gc-tag);color:var(--gc-tag-ink);font-size:.7rem;font-weight:700;letter-spacing:.02em}
.gc-tag.is-deal{background:var(--gc-deal);color:var(--gc-deal-ink)}
.gc-body{display:flex;flex:1;flex-direction:column;gap:2px;padding:12px 4px 0}
.gc-title{margin:0;font-size:.95rem;font-weight:600;line-height:1.3}
.gc-title a{color:var(--gc-ink);text-decoration:none}
.gc-title a:hover{text-decoration:underline;text-underline-offset:3px}
.gc-unit{margin:0;font-size:.78rem;color:var(--gc-muted)}
.gc-price{margin:6px 0 12px;font-size:1.15rem;font-weight:800;color:var(--gc-ink)}
.gc-price small{font-size:.75rem;font-weight:500;color:var(--gc-muted)}
.gc-foot{display:flex;align-items:center;gap:8px;margin-top:auto}
.gc-stepper{width:auto;flex-wrap:nowrap;align-items:center;height:40px;border:1.5px solid #8fa596;border-radius:999px;background:#fff}
.gc-stepper>.btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-hover-bg:var(--gc-media);--bs-btn-active-bg:var(--gc-media);margin:0!important;display:grid;place-items:center;width:34px;height:100%;padding:0;border:0;border-radius:999px!important;color:var(--gc-ink);cursor:pointer}
.gc-stepper svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2.6;stroke-linecap:round}
.gc-stepper>.form-control{flex:none;width:30px;margin:0!important;border-radius:0!important;box-shadow:none;height:100%;padding:0;border:0;background:none;color:var(--gc-ink);font:inherit;font-size:.92rem;font-weight:700;text-align:center;-moz-appearance:textfield;appearance:textfield}
.gc-stepper .form-control::-webkit-inner-spin-button,.gc-stepper .form-control::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
.gc-add{--bs-btn-padding-x:12px;--bs-btn-padding-y:0;--bs-btn-font-size:.85rem;--bs-btn-font-weight:700;--bs-btn-border-radius:999px;--bs-btn-color:#fff;--bs-btn-bg:var(--gc-green);--bs-btn-border-color:var(--gc-green);
  --bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--gc-green-dark);--bs-btn-hover-border-color:var(--gc-green-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--gc-green-dark);--bs-btn-active-border-color:var(--gc-green-dark);
  display:flex;flex:1;align-items:center;justify-content:center;gap:6px;height:40px}
.gc-add svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
/* Row tiles */
.gc-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:14px}
.gc-tile{display:grid;grid-template-columns:84px 1fr auto;align-items:center;gap:14px;padding:12px;background:#fff;border:1px solid var(--gc-line);border-radius:18px}
.gc-tile img{display:block;width:84px;height:84px;border-radius:12px;object-fit:cover;background:var(--gc-media)}
.gc-tile .gc-price{margin:4px 0 0;font-size:1.05rem}
.gc-tile-actions{display:flex;flex-direction:column;align-items:stretch;gap:6px}
.gc-tile .gc-add{flex:none;height:34px}
@media (max-width:480px){.gc-tiles{grid-template-columns:1fr}.gc-tile{grid-template-columns:72px 1fr}.gc-tile img{width:72px;height:72px}
  .gc-tile-actions{grid-column:1 / -1;flex-direction:row}.gc-tile .gc-add{flex:1;height:40px}}
:is(.gc,.gc-tiles) :is(a,.btn):focus-visible{outline:3px solid var(--gc-focus);outline-offset:2px;box-shadow:none}
.gc-stepper:focus-within{border-color:var(--gc-ink)}
.gc-stepper>.form-control:focus-visible{outline:3px solid var(--gc-focus);outline-offset:0;border-radius:6px}
@media (prefers-reduced-motion:reduce){:is(.gc,.gc-tiles) .btn{transition:none}}
JavaScript
// Quantity steppers: − and + change the number input by one, kept within its min and max.
document.querySelectorAll('[data-step]').forEach(btn => btn.addEventListener('click', () => {
  const input = btn.parentElement.querySelector('input');
  const min = Number(input.min) || 1, max = Number(input.max) || 99;
  input.value = Math.min(max, Math.max(min, (Number(input.value) || min) + Number(btn.dataset.step)));
  input.dispatchEvent(new Event('change', { bubbles: true }));
}));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5d50] uppercase">Grocery cards with a quantity stepper</span>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-[18px]">
  <li><article class="flex h-full flex-col rounded-[20px] border border-[#dfe7da] bg-white p-3">
    <div class="relative aspect-square overflow-hidden rounded-[14px] bg-[#eef3ea]"><img class="block size-full object-cover" src="https://images.unsplash.com/photo-1523049673857-eb18f1d7b578?auto=format&fit=crop&w=400&q=70" alt="Halved avocado with its stone on a pink background" width="400" height="400" loading="lazy"><span class="absolute top-2 left-2 rounded-full px-[9px] py-0.5 text-[.7rem] font-bold tracking-[.02em] bg-[#dcfce7] text-[#14532d]">Organic</span></div>
    <div class="flex flex-1 flex-col gap-0.5 px-1 pt-3">
      <h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Hass Avocados</a></h3>
      <p class="text-[.78rem] text-[#4b5d50]">Ripe and ready · each</p>
      <p class="mt-1.5 mb-3 text-[1.15rem] font-extrabold">$1.25 <small class="text-xs font-medium text-[#4b5d50]">/ each</small></p>
      <div class="mt-auto flex items-center gap-2">
        <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Hass Avocados">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="h-10 flex-1 flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none"><svg class="size-4 fill-none stroke-current stroke-[2.2] [stroke-linecap:round] [stroke-linejoin:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Hass Avocados to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col rounded-[20px] border border-[#dfe7da] bg-white p-3">
    <div class="relative aspect-square overflow-hidden rounded-[14px] bg-[#eef3ea]"><img class="block size-full object-cover" src="https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?auto=format&fit=crop&w=400&q=70" alt="Single red apple on a light grey background" width="400" height="400" loading="lazy"><span class="absolute top-2 left-2 rounded-full px-[9px] py-0.5 text-[.7rem] font-bold tracking-[.02em] bg-[#fef3c7] text-[#78350f]">2 for $6</span></div>
    <div class="flex flex-1 flex-col gap-0.5 px-1 pt-3">
      <h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Honeycrisp Apples</a></h3>
      <p class="text-[.78rem] text-[#4b5d50]">1 kg bag · $3.40/kg</p>
      <p class="mt-1.5 mb-3 text-[1.15rem] font-extrabold">$3.40 <small class="text-xs font-medium text-[#4b5d50]">/ bag</small></p>
      <div class="mt-auto flex items-center gap-2">
        <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Honeycrisp Apples">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="h-10 flex-1 flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none"><svg class="size-4 fill-none stroke-current stroke-[2.2] [stroke-linecap:round] [stroke-linejoin:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Honeycrisp Apples to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col rounded-[20px] border border-[#dfe7da] bg-white p-3">
    <div class="relative aspect-square overflow-hidden rounded-[14px] bg-[#eef3ea]"><img class="block size-full object-cover" src="https://images.unsplash.com/photo-1571771894821-ce9b6c11b08e?auto=format&fit=crop&w=400&q=70" alt="Bunch of ripe yellow bananas on a yellow background" width="400" height="400" loading="lazy"><span class="absolute top-2 left-2 rounded-full px-[9px] py-0.5 text-[.7rem] font-bold tracking-[.02em] bg-[#dcfce7] text-[#14532d]">Fair trade</span></div>
    <div class="flex flex-1 flex-col gap-0.5 px-1 pt-3">
      <h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Bananas</a></h3>
      <p class="text-[.78rem] text-[#4b5d50]">Bunch of 5–6 · about 1 kg</p>
      <p class="mt-1.5 mb-3 text-[1.15rem] font-extrabold">$1.89 <small class="text-xs font-medium text-[#4b5d50]">/ bunch</small></p>
      <div class="mt-auto flex items-center gap-2">
        <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Bananas">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="h-10 flex-1 flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none"><svg class="size-4 fill-none stroke-current stroke-[2.2] [stroke-linecap:round] [stroke-linejoin:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Bananas to cart</span></button>
      </div>
    </div>
  </article></li>
  <li><article class="flex h-full flex-col rounded-[20px] border border-[#dfe7da] bg-white p-3">
    <div class="relative aspect-square overflow-hidden rounded-[14px] bg-[#eef3ea]"><img class="block size-full object-cover" src="https://images.unsplash.com/photo-1582722872445-44dc5f7e3c8f?auto=format&fit=crop&w=400&q=70" alt="Bowl of brown eggs held in two hands" width="400" height="400" loading="lazy"><span class="absolute top-2 left-2 rounded-full px-[9px] py-0.5 text-[.7rem] font-bold tracking-[.02em] bg-[#dcfce7] text-[#14532d]">Free range</span></div>
    <div class="flex flex-1 flex-col gap-0.5 px-1 pt-3">
      <h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Large Brown Eggs</a></h3>
      <p class="text-[.78rem] text-[#4b5d50]">Box of 12 · $0.43 each</p>
      <p class="mt-1.5 mb-3 text-[1.15rem] font-extrabold">$5.20 <small class="text-xs font-medium text-[#4b5d50]">/ box</small></p>
      <div class="mt-auto flex items-center gap-2">
        <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Large Brown Eggs">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
          <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
          <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
        </div>
        <button type="button" class="h-10 flex-1 flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none"><svg class="size-4 fill-none stroke-current stroke-[2.2] [stroke-linecap:round] [stroke-linejoin:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 4h2.2l2.4 11.2h10.6L20.5 7H7"/><circle cx="9.5" cy="19.5" r="1.2"/><circle cx="17" cy="19.5" r="1.2"/></svg>Add<span class="sr-only"> Large Brown Eggs to cart</span></button>
      </div>
    </div>
  </article></li>
</ul></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4b5d50] uppercase">Row tiles for a basket or a quick-order list</span>
<ul class="grid grid-cols-[repeat(auto-fill,minmax(400px,1fr))] gap-3.5 max-[480px]:grid-cols-1">
  <li class="grid grid-cols-[84px_1fr_auto] items-center gap-3.5 rounded-[18px] border border-[#dfe7da] bg-white p-3 max-[480px]:grid-cols-[72px_1fr]">
    <img class="block size-[84px] rounded-xl bg-[#eef3ea] object-cover max-[480px]:size-[72px]" src="https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=70" alt="Round crusty loaves of bread with ears of wheat" width="84" height="84" loading="lazy">
    <div><h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Sourdough Country Loaf</a></h3><p class="text-[.78rem] text-[#4b5d50]">800 g · baked this morning</p><p class="mt-1 text-[1.05rem] font-extrabold">$6.50</p></div>
    <div class="flex flex-col items-stretch gap-1.5 max-[480px]:col-span-full max-[480px]:flex-row">
      <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Sourdough Country Loaf">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="h-[34px] flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none max-[480px]:h-10 max-[480px]:flex-1">Add<span class="sr-only"> Sourdough Country Loaf to cart</span></button>
    </div>
  </li>
  <li class="grid grid-cols-[84px_1fr_auto] items-center gap-3.5 rounded-[18px] border border-[#dfe7da] bg-white p-3 max-[480px]:grid-cols-[72px_1fr]">
    <img class="block size-[84px] rounded-xl bg-[#eef3ea] object-cover max-[480px]:size-[72px]" src="https://images.unsplash.com/photo-1592924357228-91a4daadcfea?auto=format&fit=crop&w=200&q=70" alt="Red vine tomatoes covered in water droplets on a dark background" width="84" height="84" loading="lazy">
    <div><h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Vine Tomatoes</a></h3><p class="text-[.78rem] text-[#4b5d50]">500 g · $5.98/kg</p><p class="mt-1 text-[1.05rem] font-extrabold">$2.99</p></div>
    <div class="flex flex-col items-stretch gap-1.5 max-[480px]:col-span-full max-[480px]:flex-row">
      <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Vine Tomatoes">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="2" min="1" max="20" aria-label="Quantity">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="h-[34px] flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none max-[480px]:h-10 max-[480px]:flex-1">Add<span class="sr-only"> Vine Tomatoes to cart</span></button>
    </div>
  </li>
  <li class="grid grid-cols-[84px_1fr_auto] items-center gap-3.5 rounded-[18px] border border-[#dfe7da] bg-white p-3 max-[480px]:grid-cols-[72px_1fr]">
    <img class="block size-[84px] rounded-xl bg-[#eef3ea] object-cover max-[480px]:size-[72px]" src="https://images.unsplash.com/photo-1559181567-c3190ca9959b?auto=format&fit=crop&w=200&q=70" alt="Pair of red cherries on a mint green background" width="84" height="84" loading="lazy">
    <div><h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Sweet Cherries</a></h3><p class="text-[.78rem] text-[#4b5d50]">400 g punnet · in season</p><p class="mt-1 text-[1.05rem] font-extrabold">$4.75</p></div>
    <div class="flex flex-col items-stretch gap-1.5 max-[480px]:col-span-full max-[480px]:flex-row">
      <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Sweet Cherries">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="20" aria-label="Quantity">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="h-[34px] flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none max-[480px]:h-10 max-[480px]:flex-1">Add<span class="sr-only"> Sweet Cherries to cart</span></button>
    </div>
  </li>
  <li class="grid grid-cols-[84px_1fr_auto] items-center gap-3.5 rounded-[18px] border border-[#dfe7da] bg-white p-3 max-[480px]:grid-cols-[72px_1fr]">
    <img class="block size-[84px] rounded-xl bg-[#eef3ea] object-cover max-[480px]:size-[72px]" src="https://images.unsplash.com/photo-1518977676601-b53f82aba655?auto=format&fit=crop&w=200&q=70" alt="Pile of unwashed golden potatoes" width="84" height="84" loading="lazy">
    <div><h3 class="text-[.95rem] leading-[1.3] font-semibold"><a href="#" class="underline-offset-3 hover:underline focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]">Golden Potatoes</a></h3><p class="text-[.78rem] text-[#4b5d50]">2 kg bag · $1.20/kg</p><p class="mt-1 text-[1.05rem] font-extrabold">$2.40</p></div>
    <div class="flex flex-col items-stretch gap-1.5 max-[480px]:col-span-full max-[480px]:flex-row">
      <div class="inline-flex h-10 items-center rounded-full border-[1.5px] border-[#8fa596] bg-white focus-within:border-[#1a2e1f]" role="group" aria-label="Quantity of Golden Potatoes">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="-1" aria-label="Decrease quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14"/></svg></button>
        <input class="h-full w-[30px] [appearance:textfield] bg-transparent text-center text-[.92rem] font-bold focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-[#2563eb] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" type="number" inputmode="numeric" value="1" min="1" max="10" aria-label="Quantity">
        <button type="button" class="grid h-full w-[34px] cursor-pointer place-items-center rounded-full hover:bg-[#eef3ea] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb]" data-step="1" aria-label="Increase quantity"><svg class="size-3.5 fill-none stroke-current stroke-[2.6] [stroke-linecap:round]" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
      </div>
      <button type="button" class="h-[34px] flex cursor-pointer items-center justify-center gap-1.5 rounded-full bg-[#15803d] px-3 text-[.85rem] font-bold text-white transition-colors duration-150 hover:bg-[#116632] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#2563eb] motion-reduce:transition-none max-[480px]:h-10 max-[480px]:flex-1">Add<span class="sr-only"> Golden Potatoes to cart</span></button>
    </div>
  </li>
</ul></div>
JavaScript
// Quantity steppers: − and + change the number input by one, kept within its min and max.
document.querySelectorAll('[data-step]').forEach(btn => btn.addEventListener('click', () => {
  const input = btn.parentElement.querySelector('input');
  const min = Number(input.min) || 1, max = Number(input.max) || 99;
  input.value = Math.min(max, Math.max(min, (Number(input.value) || min) + Number(btn.dataset.step)));
  input.dispatchEvent(new Event('change', { bubbles: true }));
}));

About this set

Cards for a grocery, farm shop or food delivery site, where shoppers buy several of an item and care about the unit price. Grocery cards with a quantity stepper show avocados, apples, bananas and eggs on square photos with an Organic, Fair trade, Free range or deal tag, the name, the pack size and price per kilo or per item, the price with its unit, and a footer with a quantity stepper beside a green Add button. Row tiles for a basket or a quick-order list put a small photo, the name, a freshness or weight line and the price on one row, with the stepper and the Add button stacked at the right; below 480px the controls move under the photo. The stepper is a number input between a minus and a plus button, grouped and named for the product, and the input works on its own: a shopper can type a quantity, use the arrow keys or the native spin behaviour. A few lines of JavaScript make the minus and plus buttons step the value and keep it between the input’s min and max. The Bootstrap version builds the stepper from .input-group, .btn and .form-control.

What’s included

  • Quantity stepper that is a real, labelled number input
  • Minus and plus buttons respect the input's min and max
  • Unit pricing per kilo, bunch, box or item
  • Organic, fair trade, free range and deal tags
  • Row tiles that stack their controls on narrow screens

Accessibility

Each stepper is a group named for its product, such as Quantity of Hass Avocados, holding a Decrease quantity button, a number input named Quantity and an Increase quantity button, so screen readers announce which item is being changed. Add buttons are named Add Hass Avocados to cart. The input stays usable without the script and the stepper shows a darker border while any part of it has focus, as well as a 3px focus outline on the focused control.

Customise it

Change --gc-green, --gc-tag, --gc-deal and --gc-focus on .gc and .gc-tiles, and the minmax(190px, 1fr) card width. Set min and max on each input to limit the quantity. In Tailwind edit the bg-[#15803d] and border-[#8fa596] utilities.