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.
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.
Runner Pro
Studio Buds
Time X
<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>
.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; }
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.
<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>
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
});
});
.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; } }
Shopping cart
Line items with a thumbnail, name, quantity stepper, and price — plus a subtotal and a clear checkout button. Try the +/− steppers:
<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>
// +/− 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();
});
});
.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; }
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.)
<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>
.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; }
type/inputmode for mobile keyboards, validate inline, and never demand an account before buying. See the Forms Lab and UX Best Practices (forms section).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">♥</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">♥</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">♥</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>
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.