Components

E-Commerce components

The building blocks of every online store — product cards, a product gallery, a shopping cart, and a checkout form. Each is live below with the HTML & CSS behind it, ready to adapt to your own shop.

Start here

What every store needs

An online store is really four screens working together: browse (product cards), view (gallery + details), cart (review & adjust), and checkout (pay). Get these four right and you have a working storefront.

Product cards

Scannable grid: image, name, price, rating, and a one-tap add-to-cart.

Product gallery

A big main image with thumbnails to switch the view.

Shopping cart

Line items, quantity steppers, and a clear running total.

Checkout form

The fewest fields possible, with a clear "place order" action.

Component 1

Product cards

A responsive grid of cards — image, category, title, star rating, price, and an add button. The grid auto-fills and wraps with no media queries.

Shoes

Runner Pro

★★★★★
$89
Audio

Studio Buds

★★★★☆
$129
Wearable

Time X

★★★★★
$199
HTML
<article class="pcard">
  <div class="pic"><img src="shoe.jpg" alt="Runner Pro">
    <button class="fav" aria-label="Save">♥</button></div>
  <div class="pbody">
    <span class="cat">Shoes</span>
    <h3>Runner Pro</h3>
    <div class="stars">★★★★★</div>
    <div class="prow"><span class="price">$89</span>
      <button class="add" aria-label="Add to cart">+</button></div>
  </div>
</article>
<article class="pcard">
  <div class="pic"><img src="buds.jpg" alt="Studio Buds">
    <button class="fav" aria-label="Save">♥</button></div>
  <div class="pbody">
    <span class="cat">Audio</span>
    <h3>Studio Buds</h3>
    <div class="stars">★★★★☆</div>
    <div class="prow"><span class="price">$129</span>
      <button class="add" aria-label="Add to cart">+</button></div>
  </div>
</article>
<article class="pcard">
  <div class="pic"><img src="watch.jpg" alt="Time X">
    <button class="fav" aria-label="Save">♥</button></div>
  <div class="pbody">
    <span class="cat">Wearable</span>
    <h3>Time X</h3>
    <div class="stars">★★★★★</div>
    <div class="prow"><span class="price">$199</span>
      <button class="add" aria-label="Add to cart">+</button></div>
  </div>
</article>
CSS
.products { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; }
.pcard    { background: #fff; border-radius: 14px; overflow: hidden; border: 1px solid #eceef2; }
.pcard .pic { aspect-ratio: 1; position: relative; }       /* square image area */
.pcard .prow { display: flex; justify-content: space-between; align-items: center; }
.pcard .add { background: #2563eb; color: #fff; border-radius: 8px; width: 34px; height: 34px; }
Component 2

Product gallery

A large main image with a column of thumbnails. Click a thumbnail to swap the main view — the core interaction of every product page.

HTML
<div class="gallery">
  <div class="thumbs">
    <img class="thumb active" src="1.jpg" data-full="1.jpg" alt="">
    <img class="thumb"        src="2.jpg" data-full="2.jpg" alt="">
    <img class="thumb"        src="3.jpg" data-full="3.jpg" alt="">
    <img class="thumb"        src="4.jpg" data-full="4.jpg" alt="">
  </div>
  <img class="main" id="main" src="1.jpg" alt="Product">
</div>
JavaScript
document.querySelectorAll('.thumb').forEach(t => {
  t.addEventListener('click', () => {
    document.querySelector('.thumb.active')?.classList.remove('active');
    t.classList.add('active');
    document.getElementById('main').src = t.dataset.full;   // swap main image
  });
});
CSS
.gallery { display: grid; grid-template-columns: 80px 1fr; gap: 14px; }
.thumbs  { display: flex; flex-direction: column; gap: 10px; }
.thumb   { width: 70px; height: 70px; border-radius: 10px; border: 2px solid transparent; cursor: pointer; }
.thumb.active { border-color: #2563eb; }      /* the selected thumbnail */
.main    { aspect-ratio: 1; border-radius: 14px; }
@media (max-width: 480px) { .gallery { grid-template-columns: 1fr; } .thumbs { flex-direction: row; } }
Component 3

Shopping cart

Line items with a thumbnail, name, quantity stepper, and price — plus a subtotal and a clear checkout button. Try the +/− steppers:

Runner Pro
1
$89
Studio Buds
2
$258
Subtotal$347
ShippingFree
Total$347
HTML
<div class="line">
  <img class="th" src="shoe.jpg" alt="">
  <div><span class="nm">Runner Pro</span>
    <div class="qty"><button>−</button><span class="q">1</span><button>+</button></div>
  </div>
  <span class="lp" data-price="89">$89</span>
</div>
<div class="line">
  <img class="th" src="buds.jpg" alt="">
  <div><span class="nm">Studio Buds</span>
    <div class="qty"><button>−</button><span class="q">2</span><button>+</button></div>
  </div>
  <span class="lp" data-price="129">$258</span>
</div>
<div class="foot">
  <div class="sub"><span>Subtotal</span><span id="subtotal">$347</span></div>
  <div class="sub"><span>Shipping</span><span>Free</span></div>
  <div class="total"><span>Total</span><span id="total">$347</span></div>
  <button class="checkout">Checkout</button>
</div>
JavaScript
// +/− adjusts quantity and recomputes the line + total
cart.querySelectorAll('.qty button').forEach(btn => {
  btn.addEventListener('click', () => {
    const line = btn.closest('.line');
    const q = line.querySelector('.q');
    const next = Math.max(1, +q.textContent + (+btn.dataset.d));
    q.textContent = next;
    const unit = +line.querySelector('.lp').dataset.price;
    line.querySelector('.lp').textContent = '$' + (unit * next);
    recomputeTotal();
  });
});
CSS
.cart .line { display: grid; grid-template-columns: 52px 1fr auto; gap: 12px;
  align-items: center; padding: 14px; border-bottom: 1px solid #f1f2f5; }
.qty { display: inline-flex; align-items: center; gap: 8px; }
.qty button { width: 24px; height: 24px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; }
.total { display: flex; justify-content: space-between; font-weight: 800; font-size: 1.05rem; }
.checkout { width: 100%; border: none; background: #22c55e; color: #fff; font-weight: 700; padding: 13px; border-radius: 10px; cursor: pointer; }
Component 4

Checkout form

Keep it short: contact, shipping, and payment in clearly grouped fields, with one obvious "place order" button. (This is a demo — never collect real card numbers without a secure payment processor.)

Payment is encrypted & handled by your processor
HTML
<form class="checkout-form">
  <div><label>Email</label><input type="email" placeholder="you@example.com"></div>
  <div><label>Full name</label><input type="text" placeholder="Ada Lovelace"></div>
  <div><label>Address</label><input type="text" placeholder="123 Main St"></div>
  <div class="row2">
    <div><label>City</label><input type="text" placeholder="Annandale"></div>
    <div><label>ZIP</label><input type="text" placeholder="22003" inputmode="numeric"></div>
  </div>
  <div class="pay">🔒 Payment is encrypted & handled by your processor</div>
  <button class="place">Place order — $347</button>
</form>
CSS
.checkout-form { display: grid; gap: 12px; max-width: 460px; }
.checkout-form .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }   /* city + ZIP side by side */
.checkout-form label { font-size: .72rem; font-weight: 700; text-transform: uppercase; color: #374151; }
.checkout-form input { width: 100%; padding: 11px 13px; border: 1px solid #d1d5db; border-radius: 9px; }
.checkout-form input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.15); }
.checkout-form .place { border: none; background: #2563eb; color: #fff; font-weight: 800; padding: 14px; border-radius: 10px; cursor: pointer; }
Checkout = where sales are won or lost: use the right type/inputmode for mobile keyboards, validate inline, and never demand an account before buying. See the Forms Lab and UX Best Practices (forms section).
Full Page

The whole storefront as one file

All four components — product cards, gallery, cart, and checkout — combined into one complete HTML file. Copy it into a new file called store.html, open it in a browser, and it just works — no libraries, no build step. Here's the live result, then the full code.

Live preview

Complete HTML — copy this whole file

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shop · Product, Cart & Checkout</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1d1d1f; background: #f6f7f9; line-height: 1.6; }
    .wrap { max-width: 980px; margin: 0 auto; padding: 28px 20px 60px; }
    h1 { font-size: 1.8rem; margin-bottom: 4px; }
    h2 { font-size: 1.15rem; margin: 34px 0 14px; }
    .lede { color: #6b7280; margin-bottom: 8px; }

    /* ===== Product cards ===== */
    .products { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px; }
    .pcard { background: #fff; border-radius: 14px; overflow: hidden; border: 1px solid #eceef2; box-shadow: 0 6px 18px rgba(20,30,60,.06); }
    .pcard .pic { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; position: relative; font-size: 2.6rem; }
    .pcard .fav { position: absolute; top: 8px; right: 8px; width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,.85); border: none; color: #6b7280; cursor: pointer; font-size: 1rem; }
    .pcard .fav.on { color: #ef4444; }
    .pcard .pbody { padding: 14px; }
    .pcard .cat { font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; color: #2563eb; font-weight: 700; }
    .pcard h3 { font-size: .95rem; color: #111; margin: 3px 0; }
    .pcard .stars { color: #f59e0b; font-size: .78rem; }
    .pcard .prow { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
    .pcard .price { font-weight: 800; color: #111; }
    .pcard .add { border: none; background: #2563eb; color: #fff; border-radius: 8px; width: 34px; height: 34px; font-size: 1.1rem; cursor: pointer; }
    .pcard .add:hover { background: #1d4ed8; }
    .g1 { background: linear-gradient(135deg,#dbeafe,#93c5fd); }
    .g2 { background: linear-gradient(135deg,#bbf7d0,#86efac); }
    .g3 { background: linear-gradient(135deg,#fde68a,#fca5a5); }
    .g4 { background: linear-gradient(135deg,#e9d5ff,#c4b5fd); }

    /* ===== Gallery ===== */
    .gallery { display: grid; grid-template-columns: 80px 1fr; gap: 14px; max-width: 460px; }
    .gallery .thumbs { display: flex; flex-direction: column; gap: 10px; }
    .gallery .thumb { width: 70px; height: 70px; border-radius: 10px; border: 2px solid transparent; cursor: pointer; }
    .gallery .thumb.active { border-color: #2563eb; }
    .gallery .main { aspect-ratio: 1; border-radius: 14px; }

    /* ===== Cart ===== */
    .cart { max-width: 460px; background: #fff; border-radius: 14px; border: 1px solid #eceef2; overflow: hidden; }
    .cart .line { display: grid; grid-template-columns: 52px 1fr auto; gap: 12px; align-items: center; padding: 14px; border-bottom: 1px solid #f1f2f5; }
    .cart .line .th { width: 52px; height: 52px; border-radius: 8px; }
    .cart .line .nm { font-size: .9rem; color: #111; font-weight: 600; }
    .cart .line .qty { display: inline-flex; align-items: center; gap: 8px; margin-top: 4px; }
    .cart .line .qty button { width: 24px; height: 24px; border: 1px solid #d1d5db; background: #fff; border-radius: 6px; cursor: pointer; }
    .cart .line .lp { font-weight: 700; color: #111; }
    .cart .foot { padding: 16px; }
    .cart .sub { display: flex; justify-content: space-between; font-size: .9rem; color: #374151; margin-bottom: 4px; }
    .cart .total { display: flex; justify-content: space-between; font-weight: 800; color: #111; font-size: 1.05rem; margin-top: 8px; }
    .cart .checkout { width: 100%; margin-top: 12px; border: none; background: #22c55e; color: #fff; font-weight: 700; padding: 13px; border-radius: 10px; cursor: pointer; }
    .cart .checkout:hover { background: #16a34a; }

    /* ===== Checkout form ===== */
    .checkout-form { max-width: 460px; display: grid; gap: 12px; }
    .checkout-form .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    .checkout-form label { font-size: .72rem; font-weight: 700; color: #374151; text-transform: uppercase; letter-spacing: .04em; display: block; margin-bottom: 4px; }
    .checkout-form input { width: 100%; padding: 11px 13px; border: 1px solid #d1d5db; border-radius: 9px; font: inherit; }
    .checkout-form input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,.15); }
    .checkout-form .pay { display: flex; align-items: center; gap: 8px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 12px; font-size: .85rem; color: #1e3a8a; }
    .checkout-form .place { border: none; background: #2563eb; color: #fff; font-weight: 800; padding: 14px; border-radius: 10px; cursor: pointer; }
    .checkout-form .place:hover { background: #1d4ed8; }

    .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
    @media (max-width: 720px) { .cols { grid-template-columns: 1fr; } }
  </style>
</head>
<body>

  <div class="wrap">
    <h1>Northwind Goods</h1>
    <p class="lede">A tiny storefront: browse, view, cart, and checkout.</p>

    <!-- Product cards -->
    <h2>Featured products</h2>
    <div class="products">
      <article class="pcard">
        <div class="pic g1">👟<button class="fav" aria-label="Save">&hearts;</button></div>
        <div class="pbody">
          <div class="cat">Shoes</div><h3>Runner Pro</h3><div class="stars">★★★★★</div>
          <div class="prow"><span class="price">$89</span><button class="add" aria-label="Add to cart">+</button></div>
        </div>
      </article>
      <article class="pcard">
        <div class="pic g2">🎧<button class="fav" aria-label="Save">&hearts;</button></div>
        <div class="pbody">
          <div class="cat">Audio</div><h3>Studio Buds</h3><div class="stars">★★★★☆</div>
          <div class="prow"><span class="price">$129</span><button class="add" aria-label="Add to cart">+</button></div>
        </div>
      </article>
      <article class="pcard">
        <div class="pic g3">⌚<button class="fav" aria-label="Save">&hearts;</button></div>
        <div class="pbody">
          <div class="cat">Wearable</div><h3>Time X</h3><div class="stars">★★★★★</div>
          <div class="prow"><span class="price">$199</span><button class="add" aria-label="Add to cart">+</button></div>
        </div>
      </article>
    </div>

    <!-- Gallery -->
    <h2>Product gallery</h2>
    <div class="gallery" id="gallery">
      <div class="thumbs">
        <div class="thumb g1 active" data-g="g1"></div>
        <div class="thumb g2" data-g="g2"></div>
        <div class="thumb g3" data-g="g3"></div>
        <div class="thumb g4" data-g="g4"></div>
      </div>
      <div class="main g1" id="galleryMain"></div>
    </div>

    <div class="cols">
      <!-- Cart -->
      <div>
        <h2>Your cart</h2>
        <div class="cart" id="cart">
          <div class="line">
            <div class="th g1"></div>
            <div><div class="nm">Runner Pro</div>
              <div class="qty"><button data-d="-1">−</button><span class="q">1</span><button data-d="1">+</button></div></div>
            <div class="lp" data-price="89">$89</div>
          </div>
          <div class="line">
            <div class="th g2"></div>
            <div><div class="nm">Studio Buds</div>
              <div class="qty"><button data-d="-1">−</button><span class="q">2</span><button data-d="1">+</button></div></div>
            <div class="lp" data-price="129">$258</div>
          </div>
          <div class="foot">
            <div class="sub"><span>Subtotal</span><span id="subtotal">$347</span></div>
            <div class="sub"><span>Shipping</span><span>Free</span></div>
            <div class="total"><span>Total</span><span id="total">$347</span></div>
            <button class="checkout">Checkout</button>
          </div>
        </div>
      </div>

      <!-- Checkout form -->
      <div>
        <h2>Checkout</h2>
        <form class="checkout-form" onsubmit="return false">
          <div><label>Email</label><input type="email" placeholder="you@example.com" required></div>
          <div><label>Full name</label><input type="text" placeholder="Ada Lovelace" required></div>
          <div><label>Address</label><input type="text" placeholder="123 Main St" required></div>
          <div class="row2">
            <div><label>City</label><input type="text" placeholder="Annandale"></div>
            <div><label>ZIP</label><input type="text" placeholder="22003" inputmode="numeric"></div>
          </div>
          <div class="pay">🔒 Payment is encrypted & handled by your processor</div>
          <button class="place">Place order — $347</button>
        </form>
      </div>
    </div>
  </div>

  <script>
    // Favorite toggle on product cards
    document.querySelectorAll('.fav').forEach(function (btn) {
      btn.addEventListener('click', function () { btn.classList.toggle('on'); });
    });

    // Gallery: click a thumbnail to swap the main image
    (function () {
      var g = document.getElementById('gallery');
      var main = document.getElementById('galleryMain');
      g.querySelectorAll('.thumb').forEach(function (t) {
        t.addEventListener('click', function () {
          var cur = g.querySelector('.thumb.active');
          if (cur) cur.classList.remove('active');
          t.classList.add('active');
          main.className = 'main ' + t.dataset.g;
        });
      });
    })();

    // Cart: +/- adjusts quantity and recomputes the line + total
    (function () {
      var cart = document.getElementById('cart');
      function recompute() {
        var sum = 0;
        cart.querySelectorAll('.line').forEach(function (line) {
          var unit = +line.querySelector('.lp').dataset.price;
          var q = +line.querySelector('.q').textContent;
          sum += unit * q;
        });
        document.getElementById('subtotal').textContent = '$' + sum;
        document.getElementById('total').textContent = '$' + sum;
      }
      cart.querySelectorAll('.qty button').forEach(function (btn) {
        btn.addEventListener('click', function () {
          var line = btn.closest('.line');
          var q = line.querySelector('.q');
          var next = Math.max(1, +q.textContent + (+btn.dataset.d));
          q.textContent = next;
          var unit = +line.querySelector('.lp').dataset.price;
          line.querySelector('.lp').textContent = '$' + (unit * next);
          recompute();
        });
      });
    })();
  </script>
</body>
</html>
That's a real, standalone storefront. The live preview above is rendered from the exact code in the box — so what you copy is precisely what you see.
Recap

The takeaway

A storefront is four components: product cards (auto-fill grid + add button), gallery (main image + thumbnail swap), cart (line items + quantity steppers + total), and checkout (short, well-grouped form). Build them from these patterns and wire the numbers together with a little JavaScript.