Pages Medium
Customer Checkout
Three-step restaurant checkout — pickup vs delivery, time slot picker, contact + payment with tip presets — and a confirmation success screen.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(44, 26, 14, 0.08), 0 2px 6px rgba(44, 26, 14, 0.06);
--shadow-2: 0 8px 24px rgba(44, 26, 14, 0.12);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 32px 24px 64px;
}
.page-head {
text-align: center;
margin-bottom: 28px;
}
.kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--terracotta);
font-weight: 600;
margin-bottom: 6px;
}
.page-head h1 {
font-family: var(--font-display);
font-size: clamp(1.8rem, 4vw, 2.4rem);
font-weight: 700;
letter-spacing: -0.015em;
margin-bottom: 22px;
}
.steps {
display: inline-flex;
gap: 8px;
list-style: none;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
padding: 6px;
border-radius: 999px;
}
.step {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px 6px 8px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 600;
color: var(--warm-gray);
}
.step span {
width: 22px;
height: 22px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
display: grid;
place-items: center;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
}
.step.is-active {
background: var(--forest);
color: var(--bone);
}
.step.is-active span {
background: var(--gold);
color: var(--ink);
}
.step.is-done span {
background: var(--success);
color: var(--bone);
}
/* ── Layout ── */
.layout {
display: grid;
grid-template-columns: 1fr 340px;
gap: 24px;
align-items: start;
}
@media (max-width: 840px) {
.layout {
grid-template-columns: 1fr;
}
}
.card {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-lg);
padding: 28px;
box-shadow: var(--shadow-1);
}
.panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.panel h2 {
font-family: var(--font-display);
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.panel h2.h2-sub {
margin-top: 8px;
}
/* ── Choices ── */
.choices {
display: grid;
gap: 10px;
}
.choice {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: var(--r-md);
background: var(--cream);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.choice:hover {
border-color: var(--terracotta);
}
.choice input {
accent-color: var(--forest);
width: 18px;
height: 18px;
}
.choice:has(input:checked) {
border-color: var(--forest);
background: rgba(45, 74, 62, 0.06);
}
.choice-name {
font-weight: 700;
font-size: 0.95rem;
}
.choice-desc {
font-size: 0.8rem;
color: var(--warm-gray);
margin-top: 2px;
}
.choice-tag {
margin-left: auto;
font-family: var(--font-mono);
font-size: 0.8rem;
font-weight: 700;
color: var(--ink-2);
background: var(--cream-2);
padding: 4px 10px;
border-radius: 999px;
}
/* ── Fields ── */
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label,
.field-label {
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-2);
font-weight: 700;
}
.field input,
.field textarea,
textarea {
border: 1px solid rgba(44, 26, 14, 0.12);
background: var(--cream);
border-radius: var(--r-md);
padding: 11px 14px;
font-family: inherit;
font-size: 0.92rem;
color: var(--ink);
outline: none;
transition: border-color 0.15s;
resize: vertical;
}
.field input:focus,
.field textarea:focus,
textarea:focus {
border-color: var(--terracotta);
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
/* ── Slots ── */
.slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
gap: 6px;
}
.slot {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 0.84rem;
font-weight: 600;
color: var(--ink);
padding: 10px 4px;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.slot:hover:not(:disabled) {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.slot.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.slot:disabled {
background: var(--cream-2);
color: var(--warm-gray);
cursor: not-allowed;
text-decoration: line-through;
text-decoration-thickness: 1.5px;
opacity: 0.7;
}
/* ── Methods ── */
.methods {
display: grid;
gap: 8px;
}
.m-choice {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: var(--r-md);
background: var(--cream);
cursor: pointer;
}
.m-choice input {
accent-color: var(--forest);
width: 16px;
height: 16px;
}
.m-icon {
font-size: 1.15rem;
width: 22px;
text-align: center;
}
.m-name {
font-weight: 600;
font-size: 0.9rem;
}
.m-choice:has(input:checked) {
border-color: var(--forest);
background: rgba(45, 74, 62, 0.06);
}
/* ── Tips ── */
.tips {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.tip-btn {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 10px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
color: var(--ink-2);
cursor: pointer;
}
.tip-btn:hover {
border-color: var(--terracotta);
color: var(--terracotta-d);
}
.tip-btn.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
/* ── Panel foot ── */
.panel-foot {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 8px;
}
.panel-foot.center {
justify-content: center;
}
.ghost,
.primary {
border-radius: 999px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 700;
padding: 12px 22px;
cursor: pointer;
transition: background 0.15s;
}
.ghost {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.18);
color: var(--ink-2);
}
.ghost:hover:not(:disabled) {
background: var(--cream-2);
color: var(--ink);
}
.ghost:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.primary {
background: var(--forest);
color: var(--bone);
border: none;
display: inline-flex;
align-items: center;
gap: 6px;
}
.primary:hover {
background: var(--forest-d);
}
/* ── Success ── */
.panel-success {
align-items: center;
text-align: center;
padding: 8px 0;
}
.success-mark {
width: 72px;
height: 72px;
border-radius: 999px;
background: rgba(79, 122, 58, 0.15);
color: var(--success);
display: grid;
place-items: center;
margin: 8px auto 4px;
animation: pop 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes pop {
from {
transform: scale(0.4);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.success-desc {
font-size: 0.95rem;
color: var(--ink-2);
max-width: 420px;
line-height: 1.55;
}
.success-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 18px;
width: 100%;
margin: 18px 0 6px;
padding: 16px 18px;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
}
.success-meta div {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.success-meta dt {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
font-weight: 700;
}
.success-meta dd {
font-family: var(--font-mono);
font-weight: 700;
color: var(--ink);
}
/* ── Sidebar summary ── */
.summary {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-lg);
padding: 22px;
display: flex;
flex-direction: column;
gap: 14px;
position: sticky;
top: 16px;
box-shadow: var(--shadow-1);
}
.summary header h3 {
font-family: var(--font-display);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.summary-meta {
font-size: 0.78rem;
color: var(--warm-gray);
margin-top: 2px;
}
.summary-lines {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.summary-lines li {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
color: var(--ink-2);
}
.summary-lines .m {
font-family: var(--font-mono);
font-weight: 600;
}
.summary-lines li.sub {
display: block;
padding-left: 16px;
font-size: 0.74rem;
font-style: italic;
color: var(--warm-gray);
margin-top: -2px;
}
.summary-totals {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 12px;
border-top: 1px dashed rgba(44, 26, 14, 0.18);
}
.summary-totals div {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
color: var(--ink-2);
}
.summary-totals dd {
font-family: var(--font-mono);
font-weight: 600;
}
.summary-totals .big {
margin-top: 6px;
padding-top: 10px;
border-top: 1px solid rgba(44, 26, 14, 0.16);
font-size: 1rem;
color: var(--ink);
font-weight: 700;
}
.summary-totals .big dd {
font-size: 1.2rem;
}
.tip-tag {
font-family: var(--font-mono);
font-size: 0.66rem;
background: var(--cream-2);
color: var(--ink-2);
padding: 2px 7px;
border-radius: 999px;
margin-left: 4px;
font-weight: 700;
}
/* Visibility guard: honor the [hidden] attribute over base display */
.field[hidden],
.panel[hidden] {
display: none;
}const SUBTOTAL = 98.0;
const TAX_RATE = 0.0825;
const DELIVERY_FEE = 4.0;
const SLOTS = [
{ label: "20:00", taken: false },
{ label: "20:15", taken: false },
{ label: "20:30", taken: true },
{ label: "20:45", taken: false },
{ label: "21:00", taken: false },
{ label: "21:15", taken: true },
{ label: "21:30", taken: false },
{ label: "21:45", taken: false },
];
let mode = "pickup";
let slot = "20:00";
let tip = 15;
let step = 1;
const stepsEl = document.getElementById("steps");
const slotsEl = document.getElementById("slots");
const addressField = document.getElementById("addressField");
const deliveryRow = document.getElementById("deliveryRow");
const sumSubtotal = document.getElementById("sumSubtotal");
const sumTax = document.getElementById("sumTax");
const sumTip = document.getElementById("sumTip");
const sumTipTag = document.getElementById("sumTipTag");
const sumTotal = document.getElementById("sumTotal");
const payAmount = document.getElementById("payAmount");
const summaryMeta = document.querySelector(".summary-meta");
function money(v) {
return `$${v.toFixed(2)}`;
}
function totals() {
const delivery = mode === "delivery" ? DELIVERY_FEE : 0;
const tax = SUBTOTAL * TAX_RATE;
const tipValue = SUBTOTAL * (tip / 100);
const total = SUBTOTAL + delivery + tax + tipValue;
return { delivery, tax, tipValue, total };
}
function renderSummary() {
const { delivery, tax, tipValue, total } = totals();
sumSubtotal.textContent = money(SUBTOTAL);
sumTax.textContent = money(tax);
sumTip.textContent = money(tipValue);
sumTipTag.textContent = `${tip}%`;
sumTotal.textContent = money(total);
deliveryRow.hidden = delivery === 0;
document.getElementById("sumDelivery").textContent = money(delivery);
payAmount.textContent = money(total);
summaryMeta.textContent = `3 items · ${mode === "pickup" ? "Pickup" : "Delivery"}`;
}
function renderSlots() {
slotsEl.innerHTML = SLOTS.map(
(s) => `
<button type="button" class="slot ${s.label === slot ? "is-active" : ""}"
data-slot="${s.label}" ${s.taken ? "disabled" : ""}>
${s.label}
</button>`
).join("");
}
function go(next) {
if (next === step) return;
step = next;
document.querySelectorAll(".panel").forEach((p) => {
p.hidden = Number(p.dataset.panel) !== step;
});
document.querySelectorAll(".step").forEach((s) => {
const n = Number(s.dataset.step);
s.classList.toggle("is-active", n === step);
s.classList.toggle("is-done", n < step);
});
if (step === 3) {
const { total } = totals();
document.getElementById("confName").textContent =
document.getElementById("name").value.split(" ")[0] || "guest";
document.getElementById("confTotal").textContent = money(total);
document.getElementById("confEmail").textContent =
document.getElementById("email").value || "—";
const eta = new Date();
const minutes = mode === "delivery" ? 50 : 25;
eta.setMinutes(eta.getMinutes() + minutes);
document.getElementById("confEta").textContent =
`${String(eta.getHours()).padStart(2, "0")}:${String(eta.getMinutes()).padStart(2, "0")}`;
}
if (step === 1) {
document.getElementById("confName").textContent = "Lina";
}
window.scrollTo({ top: 0, behavior: "smooth" });
}
document
.querySelectorAll("[data-go]")
.forEach((btn) => btn.addEventListener("click", () => go(Number(btn.dataset.go))));
document.querySelectorAll('input[name="mode"]').forEach((r) =>
r.addEventListener("change", () => {
mode = r.value;
addressField.hidden = mode !== "delivery";
renderSummary();
})
);
slotsEl.addEventListener("click", (e) => {
const btn = e.target.closest("[data-slot]");
if (!btn || btn.disabled) return;
slot = btn.dataset.slot;
renderSlots();
});
document.querySelectorAll("[data-tip]").forEach((btn) =>
btn.addEventListener("click", () => {
document.querySelectorAll("[data-tip]").forEach((b) => b.classList.remove("is-active"));
btn.classList.add("is-active");
tip = Number(btn.dataset.tip);
renderSummary();
})
);
document.getElementById("track").addEventListener("click", () => {
alert("In production this would navigate to the order-tracking page.");
});
renderSlots();
renderSummary();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Checkout · Casa Olivar</title>
</head>
<body>
<main class="page">
<header class="page-head">
<p class="kicker">Casa Olivar · Checkout</p>
<h1>Almost there.</h1>
<ol class="steps" id="steps">
<li class="step is-active" data-step="1"><span>1</span>Fulfilment</li>
<li class="step" data-step="2"><span>2</span>Pay</li>
<li class="step" data-step="3"><span>3</span>Confirmed</li>
</ol>
</header>
<div class="layout">
<section class="card">
<!-- Step 1 -->
<div class="panel" data-panel="1">
<h2>How do you want it?</h2>
<div class="choices">
<label class="choice">
<input type="radio" name="mode" value="pickup" checked />
<div>
<p class="choice-name">Pickup at restaurant</p>
<p class="choice-desc">42 Calle del Olivar · Ready in ~25 min</p>
</div>
<span class="choice-tag">Free</span>
</label>
<label class="choice">
<input type="radio" name="mode" value="delivery" />
<div>
<p class="choice-name">Delivery</p>
<p class="choice-desc">Within 3 km · 45–60 min</p>
</div>
<span class="choice-tag">+ $4.00</span>
</label>
</div>
<div class="field" id="addressField" hidden>
<label>Delivery address</label>
<input type="text" id="address" placeholder="Calle, número · piso · ciudad" />
</div>
<p class="field-label">Time slot</p>
<div class="slots" id="slots"></div>
<p class="field-label">Special instructions</p>
<textarea
id="notes"
rows="2"
maxlength="200"
placeholder="Allergies, doorbell info, etc."
></textarea>
<div class="panel-foot">
<button class="ghost" type="button" disabled>Back</button>
<button class="primary" type="button" data-go="2">Continue · Pay</button>
</div>
</div>
<!-- Step 2 -->
<div class="panel" data-panel="2" hidden>
<h2>Your details</h2>
<div class="row">
<div class="field">
<label>Full name</label>
<input type="text" id="name" value="Lina Mendoza" />
</div>
<div class="field">
<label>Phone</label>
<input type="tel" id="phone" value="+34 612 442 081" />
</div>
</div>
<div class="field">
<label>Email (for receipt)</label>
<input type="email" id="email" value="lina@casaolivar.es" />
</div>
<h2 class="h2-sub">Payment</h2>
<div class="methods">
<label class="m-choice">
<input type="radio" name="pay" value="card" checked />
<span class="m-icon">💳</span>
<span class="m-name">Card ending 4081</span>
</label>
<label class="m-choice">
<input type="radio" name="pay" value="apple" />
<span class="m-icon"></span>
<span class="m-name">Apple Pay</span>
</label>
<label class="m-choice">
<input type="radio" name="pay" value="cash" />
<span class="m-icon">💵</span>
<span class="m-name">Cash on arrival</span>
</label>
</div>
<p class="field-label">Tip</p>
<div class="tips">
<button class="tip-btn" type="button" data-tip="0">No tip</button>
<button class="tip-btn" type="button" data-tip="10">10%</button>
<button class="tip-btn is-active" type="button" data-tip="15">15%</button>
<button class="tip-btn" type="button" data-tip="20">20%</button>
</div>
<div class="panel-foot">
<button class="ghost" type="button" data-go="1">Back</button>
<button class="primary" type="button" data-go="3" id="payBtn">
<span>Pay </span><span id="payAmount">$0.00</span>
</button>
</div>
</div>
<!-- Step 3 -->
<div class="panel panel-success" data-panel="3" hidden>
<div class="success-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="32" height="32">
<path
d="M20 6 9 17l-5-5"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<h2>Order confirmed</h2>
<p class="success-desc">
Thanks, <strong id="confName">Lina</strong>. Your order
<strong id="confNumber">#0184</strong> is being prepared.
</p>
<dl class="success-meta">
<div><dt>Ready by</dt><dd id="confEta">—</dd></div>
<div><dt>Total</dt><dd id="confTotal">—</dd></div>
<div><dt>Receipt sent to</dt><dd id="confEmail">—</dd></div>
</dl>
<div class="panel-foot center">
<button class="ghost" type="button" data-go="1" id="restart">New order</button>
<button class="primary" type="button" id="track">Track order →</button>
</div>
</div>
</section>
<aside class="summary">
<header>
<h3>Order summary</h3>
<p class="summary-meta">3 items · Pickup</p>
</header>
<ul class="summary-lines">
<li><span>1× Burrata huerta</span><span class="m">$16.00</span></li>
<li><span>1× Ribeye 14oz</span><span class="m">$48.00</span></li>
<li class="sub">medium rare · truffle fries (+$4) · bone marrow (+$6)</li>
<li><span>2× Tinto natural</span><span class="m">$24.00</span></li>
</ul>
<dl class="summary-totals">
<div><dt>Subtotal</dt><dd id="sumSubtotal">$98.00</dd></div>
<div id="deliveryRow" hidden><dt>Delivery</dt><dd id="sumDelivery">$4.00</dd></div>
<div><dt>Tax (8.25%)</dt><dd id="sumTax">$8.09</dd></div>
<div id="tipRow"><dt>Tip <span class="tip-tag" id="sumTipTag">15%</span></dt><dd id="sumTip">$14.70</dd></div>
<div class="big"><dt>Total</dt><dd id="sumTotal">$120.79</dd></div>
</dl>
</aside>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Customer Checkout
The full off-table checkout flow. Step 1 picks the fulfillment mode (Pickup or Delivery) with conditional address + a time slot grid. Step 2 collects contact details, applies a tip preset, and renders the live total. Step 3 is the success state with order number, ETA, and a “Track order” CTA that pairs naturally with rest-order-tracking.
Sidebar shows the order summary at every step so the diner never loses sight of what they’re paying for.