← Component Library
E-Commerce Components

E-Commerce Patterns

The building blocks of an online store — wishlists, search, reviews, ratings, checkout, and more. Each with a live demo and the HTML & CSS.

01 · Wishlist

Save items for later

A list of saved products, each with a thumbnail and a filled heart to remove it. The heart icon is the universal “saved” signal.

Wireless headphones
Mechanical keyboard

HTML

<ul class="wishlist">
  <li>
    <img src="headphones.jpg" alt="">
    <span>Wireless headphones</span>
    <button aria-label="Remove from wishlist">♥</button>
  </li>
  <li>
    <img src="keyboard.jpg" alt="">
    <span>Mechanical keyboard</span>
    <button aria-label="Remove from wishlist">♥</button>
  </li>
</ul>

CSS

.wishlist li {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-bottom: 1px solid #f1f5f9;
}
.wishlist button { margin-left: auto; color: #ef4444; }

JavaScript

// Toggle the heart icon between filled and empty when clicked
document.querySelectorAll('.wishlist button').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var icon = btn.querySelector('i');
    if (icon.classList.contains('ph-heart-fill')) {
      icon.classList.remove('ph-heart-fill');
      icon.classList.add('ph-heart');
      btn.setAttribute('aria-label', 'Add to wishlist');
    } else {
      icon.classList.remove('ph-heart');
      icon.classList.add('ph-heart-fill');
      btn.setAttribute('aria-label', 'Remove from wishlist');
    }
  });
});
03 · Product Reviews

Show customer feedback

Each review pairs an avatar, name, star rating, and the comment. Reviews build trust, so give them room to breathe.

Jordan P.

Great quality and fast shipping — would buy again.

Sam K.

Comfortable for long calls. Battery lasts all week.

HTML

<label>Sort by
  <select class="review-sort">
    <option value="newest">Newest</option>
    <option value="rating">Highest rating</option>
    <option value="helpful">Most helpful</option>
  </select>
</label>
<div class="review-list">
  <article class="review" data-date="20260101" data-rating="4" data-helpful="0">
    <img class="avatar" src="jordan.jpg" alt="">
    <div>
      <p class="name">Jordan P.</p>
      <div class="stars" aria-label="4 out of 5 stars">★★★★☆</div>
      <p>Great quality and fast shipping — would buy again.</p>
      <button data-helpful="0">Helpful (0)</button>
    </div>
  </article>
  <article class="review" data-date="20251215" data-rating="5" data-helpful="0">
    <img class="avatar" src="sam.jpg" alt="">
    <div>
      <p class="name">Sam K.</p>
      <div class="stars" aria-label="5 out of 5 stars">★★★★★</div>
      <p>Comfortable for long calls. Battery lasts all week.</p>
      <button data-helpful="0">Helpful (0)</button>
    </div>
  </article>
</div>

CSS

.review { display: flex; gap: 12px; padding: 14px;
          border: 1px solid #e5e7eb; border-radius: 10px; }
.avatar { width: 40px; height: 40px; border-radius: 50%; }
.stars { color: #f59e0b; }

JavaScript

// "Helpful" buttons increment their count
document.querySelectorAll('[data-helpful]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var n = parseInt(btn.dataset.helpful, 10) || 0;
    btn.dataset.helpful = n + 1;
    btn.textContent = 'Helpful (' + (n + 1) + ')';
  });
});

// "Sort by" reorders the review list
var sortSelect = document.querySelector('.review-sort');
var list = document.querySelector('.review-list');
sortSelect.addEventListener('change', function () {
  var items = Array.from(list.querySelectorAll('.review'));
  items.sort(function (a, b) {
    if (sortSelect.value === 'newest') {
      return (+b.dataset.date) - (+a.dataset.date);
    }
    if (sortSelect.value === 'rating') {
      return (+b.dataset.rating) - (+a.dataset.rating);
    }
    return (+b.dataset.helpful || 0) - (+a.dataset.helpful || 0);
  });
  items.forEach(function (el) { list.appendChild(el); });
});
04 · Star Ratings

Rate at a glance

Filled vs. empty stars show a score. Always include a text label (like aria-label="4 out of 5") so it isn't lost on screen readers.

HTML

<div class="rating-input" data-rating="4" aria-label="Rate 1 to 5 stars">
  <button type="button" data-star="1">★</button>
  <button type="button" data-star="2">★</button>
  <button type="button" data-star="3">★</button>
  <button type="button" data-star="4">★</button>
  <button type="button" data-star="5">★</button>
</div>

CSS

.rating-input { display: inline-flex; gap: 2px; font-size: 1.5rem; }
.rating-input button { background: none; border: none; cursor: pointer; padding: 0; color: #d1d5db; }
.rating-input button.on { color: #f59e0b; }   /* gold when filled */

JavaScript

// Click a star to set the rating (fills 1 through N)
var rating = document.querySelector('.rating-input');
var stars = rating.querySelectorAll('button');
function paint(n) {
  stars.forEach(function (s, i) {
    s.classList.toggle('on', i < n);
  });
  rating.dataset.rating = n;
}
stars.forEach(function (btn) {
  btn.addEventListener('click', function () {
    paint(+btn.dataset.star);
  });
});
paint(+rating.dataset.rating);
05 · Checkout Form

Take payment details

A focused form with name, card number, and expiry/CVC side by side. Keep it short — every extra field loses sales.

HTML

<form class="ec-form">
  <label for="cardname">Name on card</label>
  <input id="cardname" type="text" placeholder="Jordan Parker">
  <label for="cardnum">Card number</label>
  <input id="cardnum" inputmode="numeric" placeholder="1234 5678 9012 3456">
  <div class="row">
    <div><label for="exp">Expiry</label><input id="exp" placeholder="MM/YY"></div>
    <div><label for="cvc">CVC</label><input id="cvc" inputmode="numeric" placeholder="123"></div>
  </div>
  <button class="pay" type="submit">Pay $49.00</button>
  <div class="ok"></div>
</form>

CSS

.row { display: flex; gap: 12px; }   /* expiry + CVC side by side */
input { width: 100%; padding: 10px 12px;
        border: 1px solid #cbd5e1; border-radius: 8px; }
input.error { border-color: #ef4444; background: #fef2f2; }
button { width: 100%; background: #16a34a; color: #fff; }
.ok { color: #16a34a; font-weight: 700; font-size: .88rem; margin-top: 10px; }

JavaScript

// Validate required fields on submit; mark empty ones with .error,
// show a confirmation message on success.
var form = document.querySelector('.ec-form');
form.addEventListener('submit', function (e) {
  e.preventDefault();
  var fields = form.querySelectorAll('input');
  var ok = true;
  fields.forEach(function (f) {
    if (!f.value.trim()) {
      f.classList.add('error');
      ok = false;
    } else {
      f.classList.remove('error');
    }
  });
  var msg = form.querySelector('.ok') || (function () {
    var d = document.createElement('div');
    d.className = 'ok';
    form.appendChild(d);
    return d;
  })();
  msg.textContent = ok ? 'Payment received — thank you for your order!' : '';
});
Never handle real card numbers yourself. Use a payment provider (Stripe, PayPal, Square) — they give you a secure, hosted field so card data never touches your code.
06 · Order Summary

Add it all up

Line items with a clear subtotal, shipping, and a bold total. Right-align the prices so they're easy to scan.

Headphones × 1 $39.00
Cable × 2 $10.00
Shipping$5.00
Total$54.00

HTML

<div class="order-summary">
  <div class="li" data-unit="39">
    <span>Headphones ×
      <button type="button" data-qd="-1">−</button>
      <span class="qv">1</span>
      <button type="button" data-qd="1">+</button>
    </span>
    <span class="line-total">$39.00</span>
  </div>
  <div class="li" data-unit="5">
    <span>Cable ×
      <button type="button" data-qd="-1">−</button>
      <span class="qv">2</span>
      <button type="button" data-qd="1">+</button>
    </span>
    <span class="line-total">$10.00</span>
  </div>
  <div class="li"><span>Shipping</span><span>$5.00</span></div>
  <div class="tot"><span>Total</span><span class="grand-total">$54.00</span></div>
</div>

CSS

.li, .tot {
  display: flex;
  justify-content: space-between;   /* label left, price right */
}
.tot { font-weight: 800; border-top: 1px solid #e5e7eb; padding-top: 10px; }

JavaScript

// +/- buttons update the line total and grand total live
var summary = document.querySelector('.order-summary');
var SHIPPING = 5;
function recompute() {
  var total = SHIPPING;
  summary.querySelectorAll('.li[data-unit]').forEach(function (li) {
    var unit = +li.dataset.unit;
    var qty = +li.querySelector('.qv').textContent;
    var lineTotal = unit * qty;
    li.querySelector('.line-total').textContent = '$' + lineTotal.toFixed(2);
    total += lineTotal;
  });
  summary.querySelector('.grand-total').textContent = '$' + total.toFixed(2);
}
summary.querySelectorAll('[data-qd]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var qv = btn.parentElement.querySelector('.qv');
    var next = Math.max(1, +qv.textContent + (+btn.dataset.qd));
    qv.textContent = next;
    recompute();
  });
});
07 · Coupon Code

Apply a discount

A single input plus an Apply button. A dashed border is a common visual cue that says “promo code.”

HTML

<form class="coupon-form">
  <input type="text" class="coupon-input" aria-label="Coupon code" placeholder="SAVE10">
  <button type="submit">Apply</button>
</form>
<div class="coupon-msg"></div>

CSS

.coupon-form { display: flex; gap: 8px; }
.coupon-form input {
  flex: 1;
  border: 1px dashed #94a3b8;       /* promo-code cue */
  text-transform: uppercase;
}
.coupon-msg.ok  { color: #16a34a; }
.coupon-msg.err { color: #ef4444; }

JavaScript

// Validate the coupon against a list and show feedback
var COUPONS = { SAVE10: 0.10, SAVE20: 0.20 };
var couponForm = document.querySelector('.coupon-form');
var couponInput = couponForm.querySelector('.coupon-input');
var couponMsg = document.querySelector('.coupon-msg');
couponForm.addEventListener('submit', function (e) {
  e.preventDefault();
  var code = couponInput.value.trim().toUpperCase();
  if (COUPONS[code]) {
    couponMsg.textContent = 'Coupon applied! ' + (COUPONS[code] * 100) + '% off.';
    couponMsg.className = 'coupon-msg ok';
  } else {
    couponMsg.textContent = 'That code is not valid.';
    couponMsg.className = 'coupon-msg err';
  }
});
09 · Recently Viewed

Pick up where they left off

Same horizontal card row, but populated from the products a visitor recently looked at — a gentle nudge back toward buying.

HTML

<section aria-label="Recently viewed">
  <h3>Recently viewed</h3>
  <div class="recently-viewed"><!-- populated by JS --></div>
</section>
<button type="button" data-view='{"nm":"Headphones","pr":"$39"}'>View Headphones</button>
<button type="button" data-view='{"nm":"Keyboard","pr":"$59"}'>View Keyboard</button>
<button type="button" data-view='{"nm":"Mouse","pr":"$25"}'>View Mouse</button>

CSS

.recently-viewed { display: flex; gap: 12px; overflow-x: auto; }
.recently-viewed .card { flex: 0 0 130px; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px; }
.recently-viewed .img { height: 72px; border-radius: 6px; background: linear-gradient(135deg,#e0f2fe,#bae6fd); margin-bottom: 8px; }

JavaScript

// On load, read recently_viewed from localStorage and render thumbnails.
// When a product is "viewed" (button click), push it onto the list.
var KEY = 'recently_viewed';
var rv = document.querySelector('.recently-viewed');

function render() {
  var items = JSON.parse(localStorage.getItem(KEY) || '[]');
  rv.innerHTML = items.map(function (p) {
    return '<div class="ec-pcard card">' +
             '<div class="img"></div>' +
             '<div class="nm">' + p.nm + '</div>' +
             '<div class="pr">' + p.pr + '</div>' +
           '</div>';
  }).join('');
}

document.querySelectorAll('[data-view]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var product = JSON.parse(btn.dataset.view);
    var items = JSON.parse(localStorage.getItem(KEY) || '[]');
    items = items.filter(function (p) { return p.nm !== product.nm; });
    items.unshift(product);
    items = items.slice(0, 6);
    localStorage.setItem(KEY, JSON.stringify(items));
    render();
  });
});

document.querySelectorAll('[data-clear-rv]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    localStorage.removeItem(KEY);
    render();
  });
});

render();
Store the list in localStorage as the user views products, then render the cards from it — a small amount of JavaScript.
10 · Full Page

The whole thing as one file

Everything above — search, reviews, star ratings, a checkout form, order summary, coupon, related products and a wishlist — combined into one complete HTML file. Copy it into a new file called shop.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>Aura Headphones — Product Page</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1f2433; background: #f6f7fb; line-height: 1.6; }
    .wrap { max-width: 760px; margin: 0 auto; padding: 24px 18px 60px; }

    /* ===== Search ===== */
    .search { position: relative; max-width: 100%; margin-bottom: 24px; }
    .search input {
      width: 100%; padding: 11px 16px 11px 40px;   /* room for the icon */
      border: 1px solid #cbd5e1; border-radius: 999px; font-size: .95rem;
    }
    .search input:focus { outline: 2px solid #38bdf8; border-color: #38bdf8; }
    .search .ico { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: #9aa3b2; }

    /* ===== Product header ===== */
    .product { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 20px; display: flex; gap: 18px; align-items: flex-start; }
    .product .img { width: 120px; height: 120px; flex: none; border-radius: 12px; background: linear-gradient(135deg,#e0f2fe,#bae6fd); }
    .product h1 { font-size: 1.4rem; }
    .product .price { color: #0369a1; font-weight: 800; font-size: 1.2rem; margin: 4px 0 6px; }
    .wishbtn { background: none; border: 1px solid #e5e7eb; border-radius: 8px; padding: 7px 12px; cursor: pointer; font-size: .85rem; color: #6b7280; }
    .wishbtn.on { color: #ef4444; border-color: #fecaca; background: #fef2f2; }

    /* ===== Star ratings ===== */
    .stars { display: inline-flex; gap: 2px; font-size: 1.1rem; }
    .stars .full  { color: #f59e0b; }   /* gold */
    .stars .empty { color: #d1d5db; }   /* grey */

    .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 20px; margin-top: 18px; }
    .card h2 { font-size: 1.05rem; margin-bottom: 12px; }

    /* ===== Reviews ===== */
    .review { display: flex; gap: 12px; padding: 12px 0; border-top: 1px solid #f1f5f9; }
    .review:first-of-type { border-top: none; padding-top: 0; }
    .review .avatar { width: 40px; height: 40px; flex: none; border-radius: 50%; background: linear-gradient(135deg,#38bdf8,#9b7bff); }
    .review .name { font-weight: 700; font-size: .9rem; }
    .review p { color: #6b7280; font-size: .88rem; margin-top: 2px; }

    /* ===== Order summary ===== */
    .line, .total { display: flex; justify-content: space-between; font-size: .9rem; color: #374151; padding: 5px 0; }
    .total { font-weight: 800; color: #111; border-top: 1px solid #e5e7eb; margin-top: 8px; padding-top: 10px; }

    /* ===== Coupon ===== */
    .coupon { display: flex; gap: 8px; margin-top: 14px; }
    .coupon input {
      flex: 1; padding: 10px 12px; border: 1px dashed #94a3b8;   /* promo-code cue */
      border-radius: 8px; text-transform: uppercase; letter-spacing: .08em; font-size: .9rem;
    }
    .coupon button { background: #0b1a33; color: #fff; border: none; border-radius: 8px; padding: 10px 18px; font-weight: 600; cursor: pointer; }

    /* ===== Checkout form ===== */
    .form label { display: block; font-size: .8rem; font-weight: 600; color: #374151; margin: 0 0 6px; }
    .form input { width: 100%; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 12px; font-size: .9rem; }
    .form input:focus { outline: 2px solid #38bdf8; border-color: #38bdf8; }
    .form .row { display: flex; gap: 12px; }   /* expiry + CVC side by side */
    .form .row > div { flex: 1; }
    .pay { width: 100%; background: #16a34a; color: #fff; border: none; border-radius: 8px; padding: 12px; font-weight: 700; font-size: .95rem; cursor: pointer; }

    /* ===== Related products ===== */
    .related { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 6px; }
    .related .pcard { flex: 0 0 130px; background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px; }
    .related .pcard .ph { height: 72px; border-radius: 6px; background: linear-gradient(135deg,#e0f2fe,#bae6fd); margin-bottom: 8px; }
    .related .pcard .nm { font-size: .8rem; font-weight: 600; }
    .related .pcard .pr { font-size: .82rem; color: #0369a1; font-weight: 700; margin-top: 2px; }

    .ok { color: #16a34a; font-weight: 700; font-size: .88rem; margin-top: 10px; min-height: 20px; }
  </style>
</head>
<body>
  <div class="wrap">

    <!-- Search -->
    <form class="search" role="search">
      <span class="ico">🔍</span>
      <input type="search" aria-label="Search products" placeholder="Search products…">
    </form>

    <!-- Product + wishlist + rating -->
    <div class="product">
      <div class="img"></div>
      <div>
        <h1>Aura Wireless Headphones</h1>
        <div class="price">$49.00</div>
        <div class="stars" aria-label="4 out of 5 stars">
          <span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="empty">☆</span>
        </div>
        <p style="font-size:.9rem;color:#6b7280;margin:8px 0 12px;">Crisp sound, 30-hour battery, and feather-light comfort.</p>
        <button class="wishbtn" id="wish" type="button" aria-pressed="false">♡ Save</button>
      </div>
    </div>

    <!-- Reviews -->
    <div class="card">
      <h2>Customer reviews</h2>
      <article class="review">
        <div class="avatar"></div>
        <div>
          <p class="name">Jordan P.</p>
          <div class="stars" aria-label="5 out of 5 stars"><span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="full">★</span></div>
          <p>Great quality and fast shipping — would buy again.</p>
        </div>
      </article>
      <article class="review">
        <div class="avatar"></div>
        <div>
          <p class="name">Sam K.</p>
          <div class="stars" aria-label="4 out of 5 stars"><span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="full">★</span><span class="empty">☆</span></div>
          <p>Comfortable for long calls. Battery lasts all week.</p>
        </div>
      </article>
    </div>

    <!-- Order summary + coupon -->
    <div class="card">
      <h2>Order summary</h2>
      <div class="line"><span>Headphones × 1</span><span>$49.00</span></div>
      <div class="line"><span>Shipping</span><span>$5.00</span></div>
      <div class="line" id="discount" style="display:none;color:#16a34a;"><span>Discount (SAVE10)</span><span>-$4.90</span></div>
      <div class="total"><span>Total</span><span id="total">$54.00</span></div>
      <form class="coupon" id="couponForm">
        <input type="text" id="code" aria-label="Coupon code" placeholder="SAVE10">
        <button type="submit">Apply</button>
      </form>
      <div class="ok" id="couponMsg"></div>
    </div>

    <!-- Checkout form -->
    <div class="card">
      <h2>Checkout</h2>
      <form class="form" id="checkout">
        <label for="cardname">Name on card</label>
        <input id="cardname" type="text" placeholder="Jordan Parker" required>
        <label for="cardnum">Card number</label>
        <input id="cardnum" type="text" inputmode="numeric" placeholder="1234 5678 9012 3456" required>
        <div class="row">
          <div><label for="exp">Expiry</label><input id="exp" type="text" placeholder="MM/YY" required></div>
          <div><label for="cvc">CVC</label><input id="cvc" type="text" inputmode="numeric" placeholder="123" required></div>
        </div>
        <button class="pay" type="submit">Pay</button>
        <div class="ok" id="payMsg"></div>
      </form>
    </div>

    <!-- Related products -->
    <div class="card">
      <h2>You might also like</h2>
      <div class="related">
        <div class="pcard"><div class="ph"></div><div class="nm">Earbuds</div><div class="pr">$29</div></div>
        <div class="pcard"><div class="ph"></div><div class="nm">Charger</div><div class="pr">$15</div></div>
        <div class="pcard"><div class="ph"></div><div class="nm">Case</div><div class="pr">$12</div></div>
        <div class="pcard"><div class="ph"></div><div class="nm">Stand</div><div class="pr">$22</div></div>
      </div>
    </div>

  </div>

  <script>
    // Wishlist toggle
    var wish = document.getElementById('wish');
    wish.addEventListener('click', function () {
      var on = wish.classList.toggle('on');
      wish.setAttribute('aria-pressed', on);
      wish.innerHTML = on ? '♥ Saved' : '♡ Save';
    });

    // Coupon applies a 10% discount
    document.getElementById('couponForm').addEventListener('submit', function (e) {
      e.preventDefault();
      var code = document.getElementById('code').value.trim().toUpperCase();
      var msg = document.getElementById('couponMsg');
      if (code === 'SAVE10') {
        document.getElementById('discount').style.display = 'flex';
        document.getElementById('total').textContent = '$49.10';
        msg.textContent = 'Coupon applied! You saved $4.90.';
      } else {
        document.getElementById('discount').style.display = 'none';
        document.getElementById('total').textContent = '$54.00';
        msg.textContent = code ? 'That code is not valid.' : '';
        msg.style.color = code ? '#ef4444' : '#16a34a';
      }
    });

    // Checkout (demo only)
    document.getElementById('checkout').addEventListener('submit', function (e) {
      e.preventDefault();
      document.getElementById('payMsg').textContent = 'Payment received — thank you for your order!';
    });
  </script>
</body>
</html>
This is a demo. Never handle real card numbers yourself — use a payment provider (Stripe, PayPal, Square) so card data never touches your code.
11 · Related

Keep building

See the E-Commerce Components page, build Cards and Forms — or browse the full Component Library.