Pages Hard
Checkout Page
A multi-step checkout flow: cart review → shipping → payment → confirmation. Client-side validation and step indicator. No libraries.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
color: #111;
min-height: 100vh;
}
.checkout-header {
background: #fff;
border-bottom: 1px solid #e5e7eb;
padding: 16px 40px;
display: flex;
flex-direction: column;
gap: 16px;
}
.logo {
font-size: 20px;
font-weight: 800;
text-decoration: none;
color: #111;
}
/* Step indicator */
.step-indicator {
display: flex;
align-items: center;
gap: 4px;
justify-content: center;
}
.step {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #aaa;
font-weight: 500;
}
.step.active {
color: #6366f1;
}
.step.done {
color: #16a34a;
}
.step-num {
width: 24px;
height: 24px;
border-radius: 50%;
background: #e5e7eb;
color: #888;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.step.active .step-num {
background: #6366f1;
color: #fff;
}
.step.done .step-num {
background: #16a34a;
color: #fff;
}
.step-label {
display: none;
}
@media (min-width: 480px) {
.step-label {
display: inline;
}
}
.step-line {
flex: 1;
max-width: 40px;
height: 1px;
background: #e5e7eb;
}
/* Layout */
.checkout-layout {
display: grid;
grid-template-columns: 1fr 340px;
gap: 32px;
max-width: 960px;
margin: 32px auto;
padding: 0 24px;
align-items: start;
}
@media (max-width: 768px) {
.checkout-layout {
grid-template-columns: 1fr;
}
.order-summary {
order: -1;
position: static;
}
.checkout-header {
padding: 16px 20px;
}
}
/* Step panels */
.step-panel {
display: none;
background: #fff;
border-radius: 16px;
border: 1px solid #e5e7eb;
padding: 28px;
}
.step-panel.active {
display: block;
}
.step-panel h2 {
font-size: 20px;
font-weight: 800;
margin-bottom: 24px;
}
/* Cart */
.cart-items {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 20px;
}
.cart-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f9fafb;
border-radius: 12px;
}
.cart-thumb {
width: 56px;
height: 56px;
border-radius: 10px;
flex-shrink: 0;
}
.t1 {
background: linear-gradient(135deg, #c8b89a, #a8946e);
}
.t2 {
background: linear-gradient(135deg, #374151, #1f2937);
}
.cart-item-info {
flex: 1;
}
.cart-item-info strong {
font-size: 14px;
display: block;
}
.cart-item-info span {
font-size: 12px;
color: #888;
}
.cart-qty {
display: flex;
align-items: center;
gap: 8px;
}
.qty-btn {
width: 28px;
height: 28px;
border: 1px solid #e5e7eb;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.cart-price {
font-size: 15px;
font-weight: 700;
min-width: 64px;
text-align: right;
}
.remove-btn {
background: none;
border: none;
font-size: 20px;
color: #aaa;
cursor: pointer;
padding: 4px;
}
.remove-btn:hover {
color: #ef4444;
}
.promo-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.promo-input {
flex: 1;
padding: 10px 14px;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
font-size: 14px;
outline: none;
}
.promo-input:focus {
border-color: #6366f1;
}
.promo-btn {
padding: 10px 18px;
background: #f3f4f6;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
/* Form grid */
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field.full {
grid-column: 1 / -1;
}
.field label {
font-size: 13px;
font-weight: 600;
color: #555;
}
.field input {
padding: 11px 14px;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
font-size: 14px;
outline: none;
transition: border-color 0.15s;
}
.field input:focus {
border-color: #6366f1;
}
.field input.error {
border-color: #ef4444;
}
.field-error {
font-size: 12px;
color: #ef4444;
min-height: 16px;
}
/* Shipping options */
.shipping-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.shipping-option {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
cursor: pointer;
transition: border-color 0.15s;
}
.shipping-option:has(input:checked) {
border-color: #6366f1;
background: #eef2ff;
}
.shipping-option input {
width: 16px;
height: 16px;
accent-color: #6366f1;
}
.shipping-option strong {
font-size: 14px;
color: #111;
display: block;
}
.shipping-option div {
font-size: 13px;
color: #555;
}
/* Security note */
.security-note {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #888;
margin-bottom: 24px;
}
.security-note svg {
width: 14px;
height: 14px;
}
/* Nav buttons */
.nav-btns {
display: flex;
gap: 12px;
}
.next-btn {
flex: 1;
padding: 14px;
background: #111;
color: #fff;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.15s;
}
.next-btn:active {
opacity: 0.85;
}
.back-btn {
padding: 14px 18px;
background: #f3f4f6;
color: #555;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.back-btn:hover {
background: #e5e7eb;
}
/* Confirmation */
.confirm-content {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 16px;
padding: 16px 0;
}
.confirm-check {
width: 72px;
height: 72px;
}
.confirm-content h2 {
font-size: 24px;
font-weight: 800;
}
.confirm-content p {
font-size: 15px;
color: #555;
max-width: 380px;
line-height: 1.6;
}
/* Order summary sidebar */
.order-summary {
background: #fff;
border-radius: 16px;
border: 1px solid #e5e7eb;
padding: 24px;
position: sticky;
top: 120px;
}
.order-summary h3 {
font-size: 16px;
font-weight: 700;
margin-bottom: 18px;
}
.summary-items {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f3f4f6;
}
.summary-item {
display: flex;
align-items: center;
gap: 10px;
}
.item-info {
flex: 1;
}
.item-info strong {
font-size: 13px;
display: block;
}
.item-info span {
font-size: 11px;
color: #888;
}
.summary-item > span {
font-size: 14px;
font-weight: 700;
}
.summary-totals {
display: flex;
flex-direction: column;
gap: 10px;
}
.total-row {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #555;
}
.total-row.total {
font-weight: 800;
color: #111;
font-size: 17px;
padding-top: 10px;
border-top: 1px solid #f3f4f6;
}
.green {
color: #16a34a;
}const panels = document.querySelectorAll(".step-panel");
const steps = document.querySelectorAll(".step");
function goToStep(n) {
panels.forEach((p) => p.classList.remove("active"));
document.getElementById(`panel-${n}`).classList.add("active");
steps.forEach((s, i) => {
const stepNum = i + 1;
s.classList.remove("active", "done");
if (stepNum < n) s.classList.add("done");
else if (stepNum === n) s.classList.add("active");
});
// Update done step numbers to checkmarks
steps.forEach((s) => {
const num = s.querySelector(".step-num");
if (s.classList.contains("done")) num.textContent = "✓";
else num.textContent = s.dataset.step;
});
window.scrollTo({ top: 0, behavior: "smooth" });
}
// Next buttons (simple)
document.querySelectorAll("[data-next]").forEach((btn) => {
btn.addEventListener("click", () => goToStep(Number(btn.dataset.next)));
});
// Back buttons
document.querySelectorAll("[data-back]").forEach((btn) => {
btn.addEventListener("click", () => goToStep(Number(btn.dataset.back)));
});
// Shipping validation
document.getElementById("shippingNext").addEventListener("click", (e) => {
const form = document.getElementById("shippingForm");
let valid = true;
form.querySelectorAll("[required]").forEach((input) => {
const error = input.parentElement.querySelector(".field-error");
if (!input.value.trim()) {
input.classList.add("error");
if (error) error.textContent = "This field is required";
valid = false;
} else {
input.classList.remove("error");
if (error) error.textContent = "";
}
});
if (valid) goToStep(3);
});
// Card number formatting
const cardInput = document.getElementById("cardNumber");
if (cardInput) {
cardInput.addEventListener("input", () => {
let v = cardInput.value.replace(/\D/g, "").slice(0, 16);
cardInput.value = v.replace(/(\d{4})(?=\d)/g, "$1 ");
});
}
// Expiry formatting
const expiryInput = document.getElementById("expiry");
if (expiryInput) {
expiryInput.addEventListener("input", () => {
let v = expiryInput.value.replace(/\D/g, "").slice(0, 4);
if (v.length > 2) v = v.slice(0, 2) + " / " + v.slice(2);
expiryInput.value = v;
});
}
// Payment validation
document.getElementById("payBtn").addEventListener("click", (e) => {
const form = document.getElementById("paymentForm");
let valid = true;
form.querySelectorAll("[required]").forEach((input) => {
const error = input.parentElement.querySelector(".field-error");
if (!input.value.trim()) {
input.classList.add("error");
if (error) error.textContent = "Required";
valid = false;
} else {
input.classList.remove("error");
if (error) error.textContent = "";
}
});
if (valid) goToStep(4);
});
// Remove cart items
document.querySelectorAll(".remove-btn").forEach((btn) => {
btn.addEventListener("click", () => btn.closest(".cart-item").remove());
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>Checkout</title>
</head>
<body>
<header class="checkout-header">
<a href="#" class="logo">Brand</a>
<div class="step-indicator" id="stepIndicator">
<div class="step active" data-step="1"><span class="step-num">1</span><span class="step-label">Cart</span></div>
<div class="step-line"></div>
<div class="step" data-step="2"><span class="step-num">2</span><span class="step-label">Shipping</span></div>
<div class="step-line"></div>
<div class="step" data-step="3"><span class="step-num">3</span><span class="step-label">Payment</span></div>
<div class="step-line"></div>
<div class="step" data-step="4"><span class="step-num">4</span><span class="step-label">Done</span></div>
</div>
</header>
<main class="checkout-layout">
<!-- Left: step panels -->
<div class="checkout-form">
<!-- Step 1: Cart -->
<div class="step-panel active" id="panel-1">
<h2>Your Cart</h2>
<div class="cart-items">
<div class="cart-item">
<div class="cart-thumb t1"></div>
<div class="cart-item-info">
<strong>Minimal Backpack</strong>
<span>Sand · 20L</span>
</div>
<div class="cart-qty">
<button class="qty-btn">−</button>
<span>1</span>
<button class="qty-btn">+</button>
</div>
<div class="cart-price">$129.00</div>
<button class="remove-btn" aria-label="Remove">×</button>
</div>
<div class="cart-item">
<div class="cart-thumb t2"></div>
<div class="cart-item-info">
<strong>Canvas Tote</strong>
<span>Black</span>
</div>
<div class="cart-qty">
<button class="qty-btn">−</button>
<span>2</span>
<button class="qty-btn">+</button>
</div>
<div class="cart-price">$58.00</div>
<button class="remove-btn" aria-label="Remove">×</button>
</div>
</div>
<div class="promo-row">
<input type="text" placeholder="Promo code" class="promo-input" />
<button class="promo-btn">Apply</button>
</div>
<button class="next-btn" data-next="2">Continue to Shipping →</button>
</div>
<!-- Step 2: Shipping -->
<div class="step-panel" id="panel-2">
<h2>Shipping Details</h2>
<form class="form-grid" id="shippingForm" novalidate>
<div class="field">
<label>First Name</label>
<input type="text" name="firstName" placeholder="Alex" required />
<span class="field-error"></span>
</div>
<div class="field">
<label>Last Name</label>
<input type="text" name="lastName" placeholder="Morgan" required />
<span class="field-error"></span>
</div>
<div class="field full">
<label>Email</label>
<input type="email" name="email" placeholder="alex@example.com" required />
<span class="field-error"></span>
</div>
<div class="field full">
<label>Address</label>
<input type="text" name="address" placeholder="123 Main St" required />
<span class="field-error"></span>
</div>
<div class="field">
<label>City</label>
<input type="text" name="city" placeholder="San Francisco" required />
<span class="field-error"></span>
</div>
<div class="field">
<label>ZIP Code</label>
<input type="text" name="zip" placeholder="94105" required />
<span class="field-error"></span>
</div>
<div class="field full">
<label>Shipping Method</label>
<div class="shipping-options">
<label class="shipping-option">
<input type="radio" name="shipping" value="standard" checked />
<div><strong>Standard</strong> — Free (5–7 days)</div>
</label>
<label class="shipping-option">
<input type="radio" name="shipping" value="express" />
<div><strong>Express</strong> — $9.99 (2–3 days)</div>
</label>
</div>
</div>
</form>
<div class="nav-btns">
<button class="back-btn" data-back="1">← Back</button>
<button class="next-btn" data-next="3" id="shippingNext">Continue to Payment →</button>
</div>
</div>
<!-- Step 3: Payment -->
<div class="step-panel" id="panel-3">
<h2>Payment</h2>
<form class="form-grid" id="paymentForm" novalidate>
<div class="field full">
<label>Card Number</label>
<input type="text" id="cardNumber" placeholder="1234 5678 9012 3456" maxlength="19" required />
<span class="field-error"></span>
</div>
<div class="field full">
<label>Cardholder Name</label>
<input type="text" placeholder="Alex Morgan" required />
<span class="field-error"></span>
</div>
<div class="field">
<label>Expiry</label>
<input type="text" id="expiry" placeholder="MM / YY" maxlength="7" required />
<span class="field-error"></span>
</div>
<div class="field">
<label>CVV</label>
<input type="password" id="cvv" placeholder="•••" maxlength="4" required />
<span class="field-error"></span>
</div>
</form>
<div class="security-note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Secured by 256-bit SSL encryption
</div>
<div class="nav-btns">
<button class="back-btn" data-back="2">← Back</button>
<button class="next-btn" data-next="4" id="payBtn">Place Order →</button>
</div>
</div>
<!-- Step 4: Confirmation -->
<div class="step-panel" id="panel-4">
<div class="confirm-content">
<svg class="confirm-check" viewBox="0 0 80 80" fill="none">
<circle cx="40" cy="40" r="36" fill="#f0fdf4" stroke="#86efac" stroke-width="2"/>
<polyline points="24,42 36,54 56,28" stroke="#22c55e" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h2>Order Placed!</h2>
<p>Thank you! Your order <strong>#ORD-2026-8472</strong> has been confirmed. You'll receive a confirmation email shortly.</p>
<a href="#" class="next-btn" style="display:inline-block;text-align:center;max-width:280px;">Continue Shopping</a>
</div>
</div>
</div>
<!-- Right: order summary -->
<aside class="order-summary">
<h3>Order Summary</h3>
<div class="summary-items">
<div class="summary-item">
<div class="item-thumb t1"></div>
<div class="item-info"><strong>Minimal Backpack</strong><span>Sand · 20L · ×1</span></div>
<span>$129.00</span>
</div>
<div class="summary-item">
<div class="item-thumb t2"></div>
<div class="item-info"><strong>Canvas Tote</strong><span>Black · ×2</span></div>
<span>$58.00</span>
</div>
</div>
<div class="summary-totals">
<div class="total-row"><span>Subtotal</span><span>$187.00</span></div>
<div class="total-row"><span>Shipping</span><span class="green">Free</span></div>
<div class="total-row total"><span>Total</span><span>$187.00</span></div>
</div>
</aside>
</main>
<script src="script.js"></script>
</body>
</html>Checkout Page
A complete multi-step checkout flow with 4 steps: cart summary, shipping details, payment info, and order confirmation. Includes client-side form validation and a persistent order summary sidebar.
Steps
- Cart — item list with quantity adjust and remove, subtotal
- Shipping — name, address fields with client-side validation
- Payment — card number formatting, expiry, CVV mask
- Confirmation — success animation with order number
Features
- Step indicator with completed / active / pending states
- Sticky order summary sidebar on desktop, collapsible on mobile
- Real-time form validation with inline error messages
- Card number auto-formats to groups of 4 digits
When to use it
- Headless e-commerce checkout prototype
- Payment flow UI mockups and testing