Photography — Booking Flow
A four-step photography booking flow on a gallery-dark canvas: pick a session type, choose an available date and time slot, fill in shoot details, then review and confirm. A live progress bar tracks position, inline validation guards each step, back and next controls preserve entered data, and a polished confirmation screen with a booking reference wraps the journey with smooth micro-interactions throughout.
MCP
Code
:root{
--ink:#0b0b0c;
--bg:#101012;
--surface:#17171b;
--surface-2:#1e1e23;
--paper:#f6f4ef;
--sand:#c9b79c;
--sand-d:#a8916f;
--muted:#9a9aa2;
--line:rgba(255,255,255,0.10);
--line-2:rgba(255,255,255,0.18);
--white:#fff;
--danger:#e98b7d;
--ok:#8fd0a4;
--r-sm:6px;
--r-md:12px;
--r-lg:18px;
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;
font-family:"Inter",system-ui,-apple-system,sans-serif;
line-height:1.5;
color:var(--paper);
background:
radial-gradient(1100px 620px at 78% -8%,rgba(201,183,156,0.10),transparent 60%),
radial-gradient(900px 520px at 0% 100%,rgba(201,183,156,0.06),transparent 55%),
var(--bg);
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
min-height:100vh;
}
.shell{max-width:920px;margin:0 auto;padding:22px 22px 40px;display:flex;flex-direction:column;min-height:100vh}
/* Topbar */
.topbar{display:flex;align-items:center;justify-content:space-between;padding-bottom:22px;border-bottom:1px solid var(--line)}
.brand{display:flex;align-items:center;gap:10px}
.brand-mark{width:26px;height:26px;border-radius:50%;background:conic-gradient(from 140deg,var(--sand),var(--sand-d),#6d5b41,var(--sand));box-shadow:0 0 0 4px rgba(201,183,156,0.12)}
.brand-name{font-family:"Fraunces",serif;font-size:19px;letter-spacing:.2px}
.brand-name em{font-style:normal;color:var(--sand);font-weight:600}
.topbar-label{font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
/* Wizard head */
.wizard{margin-top:30px;flex:1}
.eyebrow{margin:0 0 8px;font-size:10.5px;letter-spacing:.24em;text-transform:uppercase;color:var(--sand)}
.title{font-family:"Fraunces",serif;font-weight:600;font-size:clamp(30px,5vw,44px);line-height:1.05;margin:0}
.lede{margin:10px 0 0;color:var(--muted);font-size:14.5px}
/* Stepper */
.stepper{list-style:none;display:flex;gap:6px;padding:0;margin:30px 0 14px;flex-wrap:wrap}
.step{display:flex;align-items:center;gap:9px;flex:1;min-width:120px}
.step .dot{width:26px;height:26px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:700;color:var(--muted);background:var(--surface);border:1px solid var(--line);transition:.25s}
.step .lab{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);transition:.25s}
.step.is-active .dot,.step.is-done .dot{color:var(--ink);background:var(--sand);border-color:var(--sand)}
.step.is-active .lab,.step.is-done .lab{color:var(--paper)}
.step.is-done .dot{background:var(--sand-d);border-color:var(--sand-d)}
/* Progress */
.progress{height:4px;border-radius:99px;background:var(--surface-2);overflow:hidden;margin-bottom:26px}
.progress-fill{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,var(--sand-d),var(--sand));transition:width .45s cubic-bezier(.4,0,.2,1)}
/* Panels */
.panel{display:none;animation:rise .4s cubic-bezier(.2,.7,.2,1) both}
.panel.is-active{display:block}
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.panel-title{font-family:"Fraunces",serif;font-weight:400;font-size:21px;margin:0 0 16px}
.panel-title.mt{margin-top:26px}
/* Step 1 cards */
.cards{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
.card{position:relative;text-align:left;padding:0;border:1px solid var(--line);background:var(--surface);border-radius:var(--r-lg);overflow:hidden;cursor:pointer;color:inherit;font:inherit;transition:transform .2s,border-color .2s,box-shadow .2s}
.card:hover{transform:translateY(-3px);border-color:var(--line-2);box-shadow:0 16px 34px rgba(0,0,0,.4)}
.card:focus-visible{outline:2px solid var(--sand);outline-offset:2px}
.card[aria-checked="true"]{border-color:var(--sand);box-shadow:0 0 0 1px var(--sand),0 16px 34px rgba(0,0,0,.45)}
.card[aria-checked="true"]::after{content:"✓";position:absolute;top:12px;right:12px;width:24px;height:24px;border-radius:50%;background:var(--sand);color:var(--ink);display:grid;place-items:center;font-size:13px;font-weight:800}
.card-img{display:block;height:120px;background-size:cover;background-position:center;background-color:var(--surface-2);filter:saturate(.9)}
.card-body{display:block;padding:14px 15px 16px;position:relative}
.badge{position:absolute;top:-11px;left:15px;font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;font-weight:700;color:var(--ink);background:var(--sand);padding:4px 8px;border-radius:99px}
.badge-alt{background:var(--paper)}
.card-name{display:block;font-family:"Fraunces",serif;font-size:17px;margin-bottom:3px}
.card-meta{display:block;font-size:12px;color:var(--muted)}
.card-price{display:block;margin-top:9px;font-weight:700;font-size:16px;color:var(--sand)}
.card-price em{font-style:normal;font-size:11px;color:var(--muted);font-weight:500}
/* Step 2 dates + slots */
.dates{display:flex;gap:9px;overflow-x:auto;padding:2px 2px 8px;-webkit-overflow-scrolling:touch}
.date{flex:0 0 auto;width:66px;padding:11px 0;border:1px solid var(--line);background:var(--surface);border-radius:var(--r-md);text-align:center;cursor:pointer;color:inherit;font:inherit;transition:.18s}
.date:hover{border-color:var(--line-2);transform:translateY(-2px)}
.date:focus-visible{outline:2px solid var(--sand);outline-offset:2px}
.date[aria-checked="true"]{background:var(--sand);border-color:var(--sand);color:var(--ink)}
.date .dow{display:block;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.date[aria-checked="true"] .dow{color:rgba(11,11,12,.6)}
.date .dnum{display:block;font-family:"Fraunces",serif;font-size:20px;margin-top:3px}
.date .dmon{display:block;font-size:10px;color:var(--muted)}
.date[aria-checked="true"] .dmon{color:rgba(11,11,12,.6)}
.slots{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;margin-top:2px}
.slot{padding:11px 6px;border:1px solid var(--line);background:var(--surface);border-radius:var(--r-md);cursor:pointer;color:inherit;font:inherit;font-size:13.5px;font-weight:600;transition:.16s}
.slot:hover:not(:disabled){border-color:var(--sand);color:var(--sand);transform:translateY(-2px)}
.slot:focus-visible{outline:2px solid var(--sand);outline-offset:2px}
.slot[aria-checked="true"]{background:var(--sand);border-color:var(--sand);color:var(--ink)}
.slot:disabled{opacity:.32;cursor:not-allowed;text-decoration:line-through}
.hint{font-size:12.5px;color:var(--muted);margin:12px 0 0}
/* Step 3 form */
.grid{display:grid;grid-template-columns:1fr 1fr;gap:15px}
.field{display:flex;flex-direction:column;gap:7px}
.field-wide{grid-column:1/-1}
.lbl{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.lbl em{font-style:normal;text-transform:none;letter-spacing:0}
input,select,textarea{font:inherit;color:var(--paper);background:var(--surface);border:1px solid var(--line);border-radius:var(--r-md);padding:12px 13px;transition:.18s;width:100%}
textarea{resize:vertical;min-height:78px}
input::placeholder,textarea::placeholder{color:#6a6a72}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--sand);box-shadow:0 0 0 3px rgba(201,183,156,.16);background:var(--surface-2)}
input.invalid,textarea.invalid{border-color:var(--danger);box-shadow:0 0 0 3px rgba(233,139,125,.15)}
select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%239a9aa2' stroke-width='1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 13px center;padding-right:34px}
.check{flex-direction:row;align-items:flex-start;gap:11px;font-size:13px;color:var(--muted);cursor:pointer}
.check input{width:auto;flex:0 0 auto;margin-top:2px;accent-color:var(--sand)}
/* Errors */
.err{color:var(--danger);font-size:12px;margin:9px 0 0;display:flex;align-items:center;gap:6px}
.err::before{content:"!";width:15px;height:15px;flex:0 0 auto;border-radius:50%;background:var(--danger);color:var(--ink);display:inline-grid;place-items:center;font-size:10px;font-weight:800}
.field .err{margin-top:2px}
/* Step 4 review */
.review{margin:0;display:grid;gap:0}
.review .row{display:flex;justify-content:space-between;gap:16px;padding:13px 0;border-bottom:1px solid var(--line)}
.review .row dt{color:var(--muted);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;margin:0}
.review .row dd{margin:0;font-size:14px;font-weight:500;text-align:right;max-width:60%}
.total-row{display:flex;justify-content:space-between;align-items:baseline;margin-top:18px;padding-top:16px;border-top:1px solid var(--line-2)}
.total-lbl{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.total-val{font-family:"Fraunces",serif;font-size:30px;color:var(--sand)}
.fineprint{font-size:12px;color:var(--muted);margin:12px 0 0}
/* Confirmation */
.confirm{text-align:center;padding:14px 0}
.check-badge{width:64px;height:64px;margin:6px auto 18px;border-radius:50%;background:var(--sand);color:var(--ink);display:grid;place-items:center;font-size:30px;font-weight:800;box-shadow:0 0 0 8px rgba(201,183,156,.14);animation:pop .5s cubic-bezier(.2,1.4,.4,1) both}
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
.confirm-title{font-family:"Fraunces",serif;font-weight:600;font-size:30px;margin:0}
.confirm-sub{color:var(--muted);margin:8px 0 22px}
.ref{display:inline-flex;flex-direction:column;gap:5px;padding:14px 26px;border:1px dashed var(--line-2);border-radius:var(--r-md);margin-bottom:24px}
.ref-lbl{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}
.ref-code{font-family:"Fraunces",serif;font-size:24px;letter-spacing:.05em;color:var(--sand)}
.confirm .review{text-align:left;max-width:420px;margin:0 auto 24px}
/* Nav */
.nav{display:flex;align-items:center;gap:12px;margin-top:30px;padding-top:22px;border-top:1px solid var(--line)}
.nav-spacer{flex:1}
.btn{font:inherit;font-weight:600;font-size:14px;padding:12px 24px;border-radius:99px;border:1px solid transparent;cursor:pointer;transition:.18s;display:inline-flex;align-items:center;gap:8px}
.btn:focus-visible{outline:2px solid var(--sand);outline-offset:2px}
.btn-primary{background:var(--sand);color:var(--ink)}
.btn-primary:hover{background:var(--paper);transform:translateY(-1px)}
.btn-primary:active{transform:translateY(0)}
.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;background:var(--surface-2);color:var(--muted)}
.btn-ghost{background:transparent;border-color:var(--line-2);color:var(--paper)}
.btn-ghost:hover{border-color:var(--sand);color:var(--sand)}
/* Footer */
.foot{margin-top:34px;padding-top:20px;border-top:1px solid var(--line);font-size:11.5px;color:var(--muted);display:flex;gap:8px;flex-wrap:wrap}
.foot-sep{opacity:.5}
/* Toast */
.toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(20px);background:var(--surface-2);border:1px solid var(--line-2);color:var(--paper);padding:12px 20px;border-radius:99px;font-size:13.5px;box-shadow:0 18px 40px rgba(0,0,0,.5);opacity:0;transition:.3s;z-index:50}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* Responsive */
@media (max-width:520px){
.shell{padding:16px 15px 30px}
.topbar-label{display:none}
.stepper{gap:4px}
.step .lab{display:none}
.step{min-width:0;flex:0 0 auto}
.step .dot{width:30px;height:30px}
.cards{grid-template-columns:1fr}
.card-img{height:150px}
.slots{grid-template-columns:repeat(3,1fr)}
.grid{grid-template-columns:1fr}
.nav{gap:8px}
.btn{padding:12px 18px;font-size:13.5px}
.title{font-size:30px}
}(function () {
"use strict";
var TOTAL_STEPS = 4;
var state = {
step: 1,
type: null,
typeLabel: "",
price: 0,
length: "",
date: null,
dateLabel: "",
time: null,
name: "",
email: "",
phone: "",
place: "Studio — Aperture Loft",
notes: ""
};
var form = document.getElementById("bookingForm");
var panels = Array.prototype.slice.call(form.querySelectorAll(".panel"));
var steps = Array.prototype.slice.call(document.querySelectorAll(".stepper .step"));
var progressFill = document.querySelector(".progress-fill");
var progressBar = document.querySelector(".progress");
var backBtn = document.getElementById("backBtn");
var nextBtn = document.getElementById("nextBtn");
var confirmBtn = document.getElementById("confirmBtn");
var toastEl = document.getElementById("toast");
var toastTimer;
/* ---------- Toast ---------- */
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
// force reflow so transition runs
void toastEl.offsetWidth;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
setTimeout(function () { toastEl.hidden = true; }, 320);
}, 2400);
}
/* ---------- Step 1: session cards ---------- */
var cards = Array.prototype.slice.call(form.querySelectorAll(".card"));
cards.forEach(function (card) {
card.addEventListener("click", function () {
cards.forEach(function (c) { c.setAttribute("aria-checked", "false"); });
card.setAttribute("aria-checked", "true");
state.type = card.dataset.type;
state.typeLabel = card.querySelector(".card-name").textContent.trim();
state.price = parseInt(card.dataset.price, 10);
state.length = card.dataset.len;
hideError("type");
updateNav();
toast(state.typeLabel + " selected");
});
});
/* ---------- Step 2: build date strip ---------- */
var dateStrip = document.getElementById("dateStrip");
var slotGrid = document.getElementById("slotGrid");
var slotHint = document.getElementById("slotHint");
var DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var ALL_SLOTS = ["09:00", "10:30", "12:00", "13:30", "15:00", "16:30", "18:00", "19:30"];
function buildDates() {
var frag = document.createDocumentFragment();
var base = new Date();
base.setHours(0, 0, 0, 0);
for (var i = 1; i <= 10; i++) {
var d = new Date(base.getTime());
d.setDate(base.getDate() + i);
var iso = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
var label = DOW[d.getDay()] + ", " + MON[d.getMonth()] + " " + d.getDate();
var btn = document.createElement("button");
btn.type = "button";
btn.className = "date";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
btn.setAttribute("aria-label", label);
btn.dataset.iso = iso;
btn.dataset.label = label;
btn.dataset.seed = String((d.getDate() + d.getDay()) % ALL_SLOTS.length);
btn.innerHTML =
'<span class="dow">' + DOW[d.getDay()] + "</span>" +
'<span class="dnum">' + d.getDate() + "</span>" +
'<span class="dmon">' + MON[d.getMonth()] + "</span>";
btn.addEventListener("click", function (e) {
var t = e.currentTarget;
Array.prototype.forEach.call(dateStrip.children, function (c) { c.setAttribute("aria-checked", "false"); });
t.setAttribute("aria-checked", "true");
state.date = t.dataset.iso;
state.dateLabel = t.dataset.label;
state.time = null;
hideError("date");
buildSlots(parseInt(t.dataset.seed, 10));
updateNav();
});
frag.appendChild(btn);
}
dateStrip.appendChild(frag);
}
function buildSlots(seed) {
slotGrid.innerHTML = "";
slotHint.hidden = true;
ALL_SLOTS.forEach(function (time, idx) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "slot";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
btn.textContent = time;
// deterministically mark a couple slots as taken based on the date seed
var taken = ((idx + seed) % 3 === 0) || ((idx * 2 + seed) % 5 === 0);
if (taken) {
btn.disabled = true;
btn.setAttribute("aria-label", time + " — unavailable");
} else {
btn.addEventListener("click", function () {
Array.prototype.forEach.call(slotGrid.children, function (c) {
if (!c.disabled) c.setAttribute("aria-checked", "false");
});
btn.setAttribute("aria-checked", "true");
state.time = time;
hideError("time");
updateNav();
});
}
slotGrid.appendChild(btn);
});
}
buildDates();
/* ---------- Step 3: inputs ---------- */
var inputName = form.elements.name;
var inputEmail = form.elements.email;
var inputPhone = form.elements.phone;
var inputPlace = form.elements.place;
var inputNotes = form.elements.notes;
var inputAgree = form.elements.agree;
[inputName, inputEmail, inputPhone].forEach(function (el) {
el.addEventListener("input", function () {
el.classList.remove("invalid");
hideError(el.name);
updateNav();
});
});
inputAgree.addEventListener("change", function () { hideError("agree"); updateNav(); });
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
function validPhone(v) { return (v.replace(/\D/g, "").length >= 7); }
/* ---------- Error helpers ---------- */
function showError(key) {
var el = form.querySelector('[data-err="' + key + '"]');
if (el) el.hidden = false;
}
function hideError(key) {
var el = form.querySelector('[data-err="' + key + '"]');
if (el) el.hidden = true;
}
/* ---------- Step completeness (for enabling Next) ---------- */
function stepComplete(step) {
if (step === 1) return !!state.type;
if (step === 2) return !!state.date && !!state.time;
if (step === 3) {
return inputName.value.trim() &&
validEmail(inputEmail.value.trim()) &&
validPhone(inputPhone.value.trim()) &&
inputAgree.checked;
}
return true;
}
/* ---------- Validate with error messages ---------- */
function validateStep(step) {
var ok = true;
if (step === 1) {
if (!state.type) { showError("type"); ok = false; }
} else if (step === 2) {
if (!state.date) { showError("date"); ok = false; }
if (!state.time) { showError("time"); ok = false; }
} else if (step === 3) {
if (!inputName.value.trim()) { showError("name"); inputName.classList.add("invalid"); ok = false; }
if (!validEmail(inputEmail.value.trim())) { showError("email"); inputEmail.classList.add("invalid"); ok = false; }
if (!validPhone(inputPhone.value.trim())) { showError("phone"); inputPhone.classList.add("invalid"); ok = false; }
if (!inputAgree.checked) { showError("agree"); ok = false; }
}
return ok;
}
/* ---------- Navigation / rendering ---------- */
function render() {
panels.forEach(function (p) {
var isCurrent = p.dataset.panel === String(state.step);
p.hidden = !isCurrent;
p.classList.toggle("is-active", isCurrent);
});
steps.forEach(function (s) {
var n = parseInt(s.dataset.step, 10);
s.classList.toggle("is-active", n === state.step);
s.classList.toggle("is-done", n < state.step);
});
var pct = Math.round((state.step / TOTAL_STEPS) * 100);
progressFill.style.width = pct + "%";
progressBar.setAttribute("aria-valuenow", String(pct));
backBtn.hidden = state.step === 1;
nextBtn.hidden = state.step === TOTAL_STEPS;
confirmBtn.hidden = state.step !== TOTAL_STEPS;
updateNav();
window.scrollTo({ top: 0, behavior: "smooth" });
}
function updateNav() {
if (state.step < TOTAL_STEPS) {
nextBtn.disabled = !stepComplete(state.step);
}
}
function goTo(step) {
state.step = step;
if (step === 4) buildReview();
render();
}
nextBtn.addEventListener("click", function () {
// sync latest field values into state before validating step 3
if (state.step === 3) syncDetails();
if (!validateStep(state.step)) {
toast("Please complete this step");
return;
}
goTo(state.step + 1);
});
backBtn.addEventListener("click", function () {
if (state.step > 1) goTo(state.step - 1);
});
function syncDetails() {
state.name = inputName.value.trim();
state.email = inputEmail.value.trim();
state.phone = inputPhone.value.trim();
state.place = inputPlace.value;
state.notes = inputNotes.value.trim();
}
/* ---------- Review ---------- */
var reviewList = document.getElementById("reviewList");
var reviewTotal = document.getElementById("reviewTotal");
function rows(target, data) {
target.innerHTML = "";
data.forEach(function (r) {
var wrap = document.createElement("div");
wrap.className = "row";
var dt = document.createElement("dt");
dt.textContent = r[0];
var dd = document.createElement("dd");
dd.textContent = r[1];
wrap.appendChild(dt);
wrap.appendChild(dd);
target.appendChild(wrap);
});
}
function buildReview() {
syncDetails();
var data = [
["Session", state.typeLabel + " · " + state.length],
["Date", state.dateLabel],
["Time", state.time],
["Location", state.place],
["Name", state.name],
["Email", state.email],
["Phone", state.phone]
];
if (state.notes) data.push(["Notes", state.notes]);
rows(reviewList, data);
reviewTotal.textContent = "$" + state.price + (state.type === "event" ? " /hr" : "");
}
/* ---------- Confirm / submit ---------- */
var doneSummary = document.getElementById("doneSummary");
var refCode = document.getElementById("refCode");
function makeRef() {
var n = "";
for (var i = 0; i < 6; i++) n += Math.floor(Math.random() * 10);
return "APT-" + n;
}
form.addEventListener("submit", function (e) {
e.preventDefault();
// final safety validation of all gated steps
if (!state.type || !state.date || !state.time || !stepComplete(3)) {
toast("Something is missing — please review");
return;
}
refCode.textContent = makeRef();
rows(doneSummary, [
["Session", state.typeLabel],
["When", state.dateLabel + " · " + state.time],
["Total", "$" + state.price + (state.type === "event" ? " /hr" : "")],
["Confirmation sent to", state.email]
]);
// hide stepper/progress/nav, show confirmation panel
panels.forEach(function (p) {
var done = p.dataset.panel === "done";
p.hidden = !done;
p.classList.toggle("is-active", done);
});
document.getElementById("navBar").hidden = true;
steps.forEach(function (s) { s.classList.add("is-done"); s.classList.remove("is-active"); });
progressFill.style.width = "100%";
progressBar.setAttribute("aria-valuenow", "100");
toast("Booking confirmed ✓");
window.scrollTo({ top: 0, behavior: "smooth" });
});
/* ---------- Restart ---------- */
document.getElementById("restartBtn").addEventListener("click", function () {
// reset state
state.step = 1; state.type = null; state.typeLabel = ""; state.price = 0; state.length = "";
state.date = null; state.dateLabel = ""; state.time = null;
state.name = ""; state.email = ""; state.phone = ""; state.place = "Studio — Aperture Loft"; state.notes = "";
cards.forEach(function (c) { c.setAttribute("aria-checked", "false"); });
Array.prototype.forEach.call(dateStrip.children, function (c) { c.setAttribute("aria-checked", "false"); });
slotGrid.innerHTML = "";
slotHint.hidden = false;
form.reset();
[inputName, inputEmail, inputPhone].forEach(function (el) { el.classList.remove("invalid"); });
["type", "date", "time", "name", "email", "phone", "agree"].forEach(hideError);
document.getElementById("navBar").hidden = false;
render();
toast("Let's book another shoot");
});
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aperture Studio — Book a Session</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">Aperture<em>Studio</em></span>
</div>
<span class="topbar-label">New York · Available worldwide</span>
</header>
<main class="wizard" aria-live="polite">
<div class="wizard-head">
<p class="eyebrow">Book a session</p>
<h1 class="title">Reserve your shoot</h1>
<p class="lede">Four quick steps. Confirm in under two minutes.</p>
</div>
<!-- Stepper -->
<ol class="stepper" role="list">
<li class="step is-active" data-step="1"><span class="dot">1</span><span class="lab">Session</span></li>
<li class="step" data-step="2"><span class="dot">2</span><span class="lab">Date & time</span></li>
<li class="step" data-step="3"><span class="dot">3</span><span class="lab">Details</span></li>
<li class="step" data-step="4"><span class="dot">4</span><span class="lab">Review</span></li>
</ol>
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="25" aria-label="Booking progress">
<span class="progress-fill" style="width:25%"></span>
</div>
<form id="bookingForm" novalidate>
<!-- STEP 1 — Session type -->
<section class="panel is-active" data-panel="1" aria-label="Choose session type">
<h2 class="panel-title">What are we shooting?</h2>
<div class="cards" role="radiogroup" aria-label="Session type">
<button type="button" class="card" role="radio" aria-checked="false" data-type="portrait" data-price="240" data-len="60 min">
<span class="card-img" style="background-image:url('https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=640&q=60')"></span>
<span class="card-body">
<span class="badge">Popular</span>
<span class="card-name">Portrait</span>
<span class="card-meta">60 min · 1 look · 25 edited</span>
<span class="card-price">$240</span>
</span>
</button>
<button type="button" class="card" role="radio" aria-checked="false" data-type="couple" data-price="360" data-len="90 min">
<span class="card-img" style="background-image:url('https://images.unsplash.com/photo-1520854221256-17451cc331bf?auto=format&fit=crop&w=640&q=60')"></span>
<span class="card-body">
<span class="card-name">Couples</span>
<span class="card-meta">90 min · 2 looks · 40 edited</span>
<span class="card-price">$360</span>
</span>
</button>
<button type="button" class="card" role="radio" aria-checked="false" data-type="brand" data-price="520" data-len="120 min">
<span class="card-img" style="background-image:url('https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=640&q=60')"></span>
<span class="card-body">
<span class="badge badge-alt">Best value</span>
<span class="card-name">Brand & product</span>
<span class="card-meta">2 hr · studio · 60 edited</span>
<span class="card-price">$520</span>
</span>
</button>
<button type="button" class="card" role="radio" aria-checked="false" data-type="event" data-price="180" data-len="per hour">
<span class="card-img" style="background-image:url('https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=640&q=60')"></span>
<span class="card-body">
<span class="card-name">Event</span>
<span class="card-meta">Hourly · gallery in 72 hr</span>
<span class="card-price">$180<em>/hr</em></span>
</span>
</button>
</div>
<p class="err" data-err="type" hidden>Please choose a session type to continue.</p>
</section>
<!-- STEP 2 — Date & time -->
<section class="panel" data-panel="2" aria-label="Choose date and time" hidden>
<h2 class="panel-title">Pick a date</h2>
<div class="dates" id="dateStrip" role="radiogroup" aria-label="Available dates"></div>
<p class="err" data-err="date" hidden>Select an available date.</p>
<h2 class="panel-title mt">Available times</h2>
<div class="slots" id="slotGrid" role="radiogroup" aria-label="Time slots"></div>
<p class="hint" id="slotHint">Choose a date to see open times.</p>
<p class="err" data-err="time" hidden>Select a time slot.</p>
</section>
<!-- STEP 3 — Details -->
<section class="panel" data-panel="3" aria-label="Your details" hidden>
<h2 class="panel-title">Your details</h2>
<div class="grid">
<label class="field">
<span class="lbl">Full name</span>
<input type="text" name="name" autocomplete="name" placeholder="Maya Ellison" required />
<span class="err" data-err="name" hidden>Enter your name.</span>
</label>
<label class="field">
<span class="lbl">Email</span>
<input type="email" name="email" autocomplete="email" placeholder="[email protected]" required />
<span class="err" data-err="email" hidden>Enter a valid email address.</span>
</label>
<label class="field">
<span class="lbl">Phone</span>
<input type="tel" name="phone" autocomplete="tel" placeholder="(212) 555-0148" required />
<span class="err" data-err="phone" hidden>Enter a contact number (min 7 digits).</span>
</label>
<label class="field">
<span class="lbl">Location preference</span>
<select name="place">
<option value="Studio — Aperture Loft">Studio — Aperture Loft</option>
<option value="On location (outdoor)">On location (outdoor)</option>
<option value="Client venue">Client venue</option>
</select>
</label>
<label class="field field-wide">
<span class="lbl">Notes for the shoot <em>(optional)</em></span>
<textarea name="notes" rows="3" placeholder="Mood, wardrobe, references, must-have shots…"></textarea>
</label>
<label class="check field-wide">
<input type="checkbox" name="agree" required />
<span>I agree to the booking terms and 24-hour cancellation policy.</span>
</label>
<p class="err" data-err="agree" hidden>Please accept the terms to continue.</p>
</div>
</section>
<!-- STEP 4 — Review -->
<section class="panel" data-panel="4" aria-label="Review booking" hidden>
<h2 class="panel-title">Review & confirm</h2>
<dl class="review" id="reviewList"></dl>
<div class="total-row">
<span class="total-lbl">Estimated total</span>
<span class="total-val" id="reviewTotal">$0</span>
</div>
<p class="fineprint">A $50 deposit holds your slot. Balance due on shoot day.</p>
</section>
<!-- CONFIRMATION -->
<section class="panel confirm" data-panel="done" aria-label="Booking confirmed" hidden>
<div class="check-badge" aria-hidden="true">✓</div>
<h2 class="confirm-title">You're booked</h2>
<p class="confirm-sub">A confirmation is on its way to your inbox.</p>
<div class="ref">
<span class="ref-lbl">Booking reference</span>
<span class="ref-code" id="refCode">APT-000000</span>
</div>
<dl class="review" id="doneSummary"></dl>
<button type="button" class="btn btn-ghost" id="restartBtn">Book another session</button>
</section>
<!-- Nav -->
<div class="nav" id="navBar">
<button type="button" class="btn btn-ghost" id="backBtn" hidden>Back</button>
<span class="nav-spacer"></span>
<button type="button" class="btn btn-primary" id="nextBtn">Next</button>
<button type="submit" class="btn btn-primary" id="confirmBtn" hidden>Confirm booking</button>
</div>
</form>
</main>
<footer class="foot">
<span>© 2026 Aperture Studio</span>
<span class="foot-sep">·</span>
<span>Fictional demo — no real bookings taken</span>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>A complete multi-step booking experience for a fictional photography studio, built on a full-bleed gallery-dark canvas with thin uppercase labels, a display serif for headings, and warm sand accents. The flow moves through four stages — selecting a session type from image-led cards, picking an available date and time slot from a generated calendar strip, entering shoot details in a validated form, and reviewing a clean summary before confirming.
Each step is gated by inline validation: the Next button stays disabled until the current step is complete, empty or malformed fields surface accessible error messages, and the progress bar plus numbered stepper animate as you advance. Back and Next controls fully preserve entered data, so nothing is lost when you revisit an earlier step. Selected cards and slots use clear pressed states, badges call out popular packages and price, and a toast helper confirms micro-actions.
Confirming the booking swaps in a success screen showing a generated booking reference, the chosen package, date, time and contact details, plus a call to start over. The whole interface is keyboard-usable with visible focus rings, meets AA contrast on the dark surface, and collapses gracefully to a single-column layout below 520px.