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.
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');
}
});
});
Find products fast
A rounded search field with an inline icon. Use type="search" and a clear placeholder; wrap it in a role="search" form.
HTML
<form class="search" role="search"> <i class="icon"></i> <input class="search-input" type="search" aria-label="Search products" placeholder="Search products…"> </form> <div class="product-card">Headphones</div> <div class="product-card">Keyboard</div> <div class="product-card">Mouse</div> <div class="product-card">Webcam</div> <div class="no-results" hidden>No results</div>
CSS
.search { position: relative; }
.search i { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); }
.search input {
width: 100%; padding: 10px 14px 10px 38px; /* room for the icon */
border: 1px solid #cbd5e1; border-radius: 999px;
}
JavaScript
// Filter .product-card elements as the user types; show "No results" if all are hidden
var input = document.querySelector('.search-input');
var cards = document.querySelectorAll('.product-card');
var noResults = document.querySelector('.no-results');
input.addEventListener('input', function () {
var q = input.value.trim().toLowerCase();
var visible = 0;
cards.forEach(function (card) {
var match = card.textContent.toLowerCase().indexOf(q) !== -1;
card.style.display = match ? '' : 'none';
if (match) visible++;
});
noResults.style.display = visible === 0 ? 'block' : 'none';
noResults.textContent = 'No results';
});
Show customer feedback
Each review pairs an avatar, name, star rating, and the comment. Reviews build trust, so give them room to breathe.
Great quality and fast shipping — would buy again.
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); });
});
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);
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!' : '';
});
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.
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();
});
});
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';
}
});
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();
localStorage as the user views products, then render the cards from it — a small amount of JavaScript.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>
Keep building
See the E-Commerce Components page, build Cards and Forms — or browse the full Component Library.