Airline — Online Check-in
A six-step online check-in wizard for a fictional airline, styled with a clean status-forward aviation feel. Passengers find a booking by reference and last name, confirm details, pick a seat from an interactive cabin map with extra-legroom pricing, add checked bags, toggle travel and health declarations, and receive an issued boarding pass with a perforated stub and generated barcode. Built with vanilla JavaScript and a responsive, mobile-first layout.
MCP
Code
:root {
--sky: #0a66c2;
--sky-d: #084e95;
--sky-50: #e9f2fb;
--cloud: #f5f8fc;
--sunrise: #ff7a33;
--sunrise-50: #fff0e7;
--ink: #13233b;
--ink-2: #3a4d68;
--muted: #6b7c93;
--bg: #f5f8fc;
--surface: #ffffff;
--line: rgba(19, 35, 59, 0.1);
--line-2: rgba(19, 35, 59, 0.18);
--ok: #1f9d62;
--warn: #e0962a;
--danger: #d4493e;
--boarding: #1f9d62;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.06), 0 1px 3px rgba(19, 35, 59, 0.08);
--shadow-md: 0 6px 20px rgba(19, 35, 59, 0.1);
--font: "Inter", system-ui, -apple-system, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body {
margin: 0;
font-family: var(--font);
line-height: 1.5;
color: var(--ink);
background: var(--bg);
background-image:
radial-gradient(1200px 400px at 80% -10%, var(--sky-50), transparent),
radial-gradient(900px 360px at -10% 110%, var(--sunrise-50), transparent);
background-attachment: fixed;
}
.tnum, .time, .code, .bp-code, .total-amount, .bag-price, .bp-stub-val, .bp-seq {
font-variant-numeric: tabular-nums;
}
.app {
max-width: 760px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid;
place-items: center;
width: 38px; height: 38px;
border-radius: 11px;
color: #fff;
background: linear-gradient(135deg, var(--sky), var(--sky-d));
box-shadow: var(--shadow-sm);
}
.brand-name { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
.brand-name strong { color: var(--sky); }
.badge-secure {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
padding: 6px 11px; border-radius: 999px;
box-shadow: var(--shadow-sm);
}
.badge-secure svg { color: var(--ok); }
/* Stepper */
.stepper { padding: 4px 20px 8px; overflow-x: auto; }
.stepper ol {
list-style: none; margin: 0; padding: 0;
display: flex; gap: 4px; min-width: max-content;
}
.step {
display: flex; align-items: center; gap: 8px;
padding: 6px 10px; border-radius: 999px;
font-size: 12.5px; font-weight: 600; color: var(--muted);
flex: 0 0 auto;
}
.step .dot {
width: 22px; height: 22px; border-radius: 50%;
display: grid; place-items: center;
font-size: 12px; font-weight: 700;
background: var(--surface); color: var(--muted);
border: 1.5px solid var(--line-2);
transition: all .25s ease;
}
.step.is-active { color: var(--sky-d); }
.step.is-active .dot { background: var(--sky); color: #fff; border-color: var(--sky); }
.step.is-done { color: var(--ink-2); }
.step.is-done .dot { background: var(--ok); color: #fff; border-color: var(--ok); }
.step.is-done .dot::after { content: "✓"; }
.step.is-done .dot { font-size: 0; }
.step.is-done .dot::after { font-size: 12px; }
/* Stage / panels */
.stage { flex: 1; padding: 12px 20px 20px; }
.panel { display: none; animation: rise .35s ease both; }
.panel.is-active { display: block; }
@keyframes rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--shadow-sm);
margin-bottom: 14px;
}
.card-title { margin: 0 0 4px; font-size: 19px; font-weight: 700; letter-spacing: -0.02em; }
.card-sub { margin: 0 0 16px; font-size: 14px; color: var(--muted); }
.card-sub strong { color: var(--ink); }
/* Flight summary */
.flight-summary { background: linear-gradient(135deg, #fff, var(--sky-50)); }
.route { display: flex; align-items: center; gap: 12px; }
.endpoint { display: flex; flex-direction: column; }
.endpoint.align-end { align-items: flex-end; text-align: right; }
.endpoint .code { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
.endpoint .city { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
.endpoint .time { font-size: 14px; font-weight: 600; color: var(--ink-2); margin-top: 6px; }
.leg { flex: 1; display: flex; align-items: center; gap: 4px; color: var(--sky); }
.leg-line { flex: 1; height: 2px; background: repeating-linear-gradient(90deg, var(--line-2) 0 6px, transparent 6px 11px); }
.leg-plane { flex: 0 0 auto; transform: rotate(90deg); }
.flight-meta {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
margin-top: 16px; padding-top: 14px;
border-top: 1px dashed var(--line-2);
font-size: 13px; color: var(--ink-2);
}
.flight-meta strong { color: var(--ink); }
/* Pills */
.pill {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 700; letter-spacing: 0.01em;
padding: 4px 10px; border-radius: 999px;
}
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.pill-ok { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.pill-board { color: var(--boarding); background: rgba(31, 157, 98, 0.14); }
.pill-warn { color: var(--warn); background: rgba(224, 150, 42, 0.14); }
.pill-muted { color: var(--muted); background: rgba(107, 124, 147, 0.12); }
/* Forms */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 16px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field-full { grid-column: 1 / -1; }
.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.field input {
font-family: inherit; font-size: 15px; color: var(--ink);
padding: 11px 13px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
transition: border-color .15s, box-shadow .15s;
}
#ref { text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; }
.field input::placeholder { color: var(--muted); letter-spacing: normal; text-transform: none; font-weight: 400; }
.field input:focus {
outline: none;
border-color: var(--sky);
box-shadow: 0 0 0 3px rgba(10, 102, 194, 0.16);
}
.field input.invalid { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(212, 73, 62, 0.14); }
.hint { font-size: 12px; color: var(--muted); }
.prefill-note { grid-column: 1 / -1; margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.linkbtn {
background: none; border: none; padding: 0;
font: inherit; font-weight: 700; color: var(--sky);
cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.linkbtn:hover { color: var(--sky-d); }
/* Buttons */
.btn {
font-family: inherit; font-size: 15px; font-weight: 600;
padding: 12px 20px; border-radius: var(--r-sm);
border: 1.5px solid transparent; cursor: pointer;
transition: transform .08s, background .15s, box-shadow .15s, border-color .15s;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn-primary { background: var(--sky); color: #fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { background: var(--sky-d); }
.btn-primary:disabled { background: #aebfd4; cursor: not-allowed; box-shadow: none; }
.btn-ghost { background: var(--surface); color: var(--ink-2); border-color: var(--line-2); }
.btn-ghost:hover { border-color: var(--sky); color: var(--sky-d); }
.btn-block { width: 100%; }
.btn:focus-visible { outline: 3px solid rgba(10, 102, 194, 0.4); outline-offset: 2px; }
/* Passenger list */
.pax-list { list-style: none; margin: 0 0 16px; padding: 0; }
.pax-row {
display: flex; align-items: center; gap: 13px;
padding: 14px; border: 1px solid var(--line);
border-radius: var(--r-md); background: var(--cloud);
}
.avatar {
width: 42px; height: 42px; border-radius: 50%;
display: grid; place-items: center;
font-size: 14px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--sunrise), #e8631f);
flex: 0 0 auto;
}
.pax-info { flex: 1; display: flex; flex-direction: column; }
.pax-name { font-size: 15px; font-weight: 700; }
.pax-detail { font-size: 12.5px; color: var(--muted); }
.contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* Seat map */
.seat-legend {
display: flex; flex-wrap: wrap; gap: 14px;
font-size: 12.5px; color: var(--ink-2); margin-bottom: 16px;
}
.seat-legend span { display: inline-flex; align-items: center; gap: 6px; }
.sw { width: 16px; height: 16px; border-radius: 5px; display: inline-block; }
.sw-free { background: var(--sky-50); border: 1.5px solid var(--line-2); }
.sw-extra { background: var(--sunrise-50); border: 1.5px solid var(--sunrise); }
.sw-taken { background: #d7dde6; }
.sw-sel { background: var(--sky); }
.cabin {
background: var(--cloud);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 12px;
overflow-x: auto;
}
.seat-cols, .seat-row {
display: grid;
grid-template-columns: repeat(3, 38px) 22px repeat(3, 38px);
gap: 7px;
justify-content: center;
}
.seat-cols { margin-bottom: 8px; font-size: 11px; font-weight: 700; color: var(--muted); text-align: center; }
.seat-cols .aisle { width: 22px; }
.seat-row { margin-bottom: 7px; align-items: center; }
.row-num {
position: absolute; left: -2px;
font-size: 11px; font-weight: 700; color: var(--muted);
}
.seat-row { position: relative; }
.aisle-gap { width: 22px; text-align: center; font-size: 10px; color: var(--muted); }
.seat {
width: 38px; height: 38px; border: none;
border-radius: 8px 8px 6px 6px; cursor: pointer;
font-size: 11px; font-weight: 700; color: var(--sky-d);
background: var(--sky-50);
box-shadow: inset 0 -3px 0 rgba(19, 35, 59, 0.08);
transition: transform .1s, background .15s, color .15s;
}
.seat:hover:not(:disabled) { transform: translateY(-2px); }
.seat.extra { background: var(--sunrise-50); color: #c34e15; box-shadow: inset 0 -3px 0 rgba(255, 122, 51, 0.3); }
.seat.taken { background: #d7dde6; color: #97a3b4; cursor: not-allowed; box-shadow: inset 0 -3px 0 rgba(19,35,59,0.1); }
.seat.selected {
background: var(--sky); color: #fff;
box-shadow: inset 0 -3px 0 var(--sky-d), 0 4px 12px rgba(10, 102, 194, 0.35);
transform: translateY(-2px);
}
.seat:focus-visible { outline: 3px solid rgba(10, 102, 194, 0.45); outline-offset: 2px; }
.seat-status {
margin-top: 14px; font-size: 13.5px; font-weight: 500; color: var(--muted);
padding: 11px 14px; border-radius: var(--r-sm);
background: var(--cloud); border: 1px dashed var(--line-2);
}
.seat-status.chosen { color: var(--sky-d); background: var(--sky-50); border-style: solid; border-color: var(--sky); font-weight: 600; }
/* Bags */
.bag-counter {
display: flex; align-items: center; gap: 14px;
padding: 16px; border: 1px solid var(--line);
border-radius: var(--r-md); background: var(--cloud);
}
.bag-icon {
width: 50px; height: 50px; border-radius: 12px;
display: grid; place-items: center; flex: 0 0 auto;
color: var(--sky); background: var(--sky-50);
}
.bag-copy { flex: 1; display: flex; flex-direction: column; }
.bag-title { font-size: 15px; font-weight: 700; }
.bag-price { font-size: 13px; color: var(--muted); }
.stepper-ctrl { display: flex; align-items: center; gap: 4px; }
.rounder {
width: 36px; height: 36px; border-radius: 50%;
border: 1.5px solid var(--line-2); background: var(--surface);
font-size: 20px; font-weight: 600; color: var(--sky-d); cursor: pointer;
display: grid; place-items: center; line-height: 1;
transition: background .15s, border-color .15s, transform .08s;
}
.rounder:hover:not(:disabled) { background: var(--sky-50); border-color: var(--sky); }
.rounder:active { transform: scale(0.92); }
.rounder:disabled { opacity: 0.4; cursor: not-allowed; }
.bag-count { min-width: 32px; text-align: center; font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.total-row {
display: flex; justify-content: space-between; align-items: center;
margin-top: 16px; padding-top: 14px;
border-top: 1px dashed var(--line-2);
font-size: 15px; font-weight: 600;
}
.total-amount { font-size: 18px; font-weight: 800; color: var(--sky-d); }
/* Documents / switches */
.check-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.check-item {
display: flex; align-items: flex-start; gap: 14px;
padding: 14px 16px; border: 1px solid var(--line);
border-radius: var(--r-md); background: var(--cloud);
transition: border-color .15s, background .15s;
}
.check-item.on { border-color: var(--ok); background: rgba(31, 157, 98, 0.05); }
.switch { position: relative; flex: 0 0 auto; cursor: pointer; padding-top: 2px; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.track {
display: block; width: 46px; height: 26px; border-radius: 999px;
background: #c7d0db; transition: background .2s;
}
.track::after {
content: ""; position: absolute; top: 4px; left: 4px;
width: 20px; height: 20px; border-radius: 50%;
background: #fff; box-shadow: var(--shadow-sm);
transition: transform .2s;
}
.switch input:checked + .track { background: var(--ok); }
.switch input:checked + .track::after { transform: translateX(20px); }
.switch input:focus-visible + .track { box-shadow: 0 0 0 3px rgba(10, 102, 194, 0.35); }
.check-copy { display: flex; flex-direction: column; gap: 2px; }
.check-title { font-size: 14.5px; font-weight: 600; }
.check-detail { font-size: 12.5px; color: var(--muted); }
.docs-warn { margin: 14px 0 0; font-size: 13px; font-weight: 600; color: var(--danger); }
/* Boarding pass */
.bp-intro { text-align: center; margin-bottom: 16px; }
.boarding-pass {
display: flex;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-md);
animation: pop .45s cubic-bezier(.2,.9,.3,1.4) both;
}
@keyframes pop { from { opacity: 0; transform: scale(.95); } to { opacity: 1; transform: scale(1); } }
.bp-main {
flex: 1; padding: 22px;
background: linear-gradient(150deg, var(--sky), var(--sky-d));
color: #fff;
}
.bp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.bp-brand { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.bp-head .pill-board { background: rgba(255, 255, 255, 0.18); color: #fff; }
.bp-head .pill-board::before { background: #8df0bb; }
.bp-route { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
.bp-end { display: flex; flex-direction: column; }
.bp-end.align-end { align-items: flex-end; }
.bp-code { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
.bp-city { font-size: 12px; opacity: 0.85; margin-top: 3px; }
.bp-plane { flex: 1; transform: rotate(90deg); opacity: 0.9; }
.bp-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px 10px; margin: 0; }
.bp-grid div { display: flex; flex-direction: column; }
.bp-grid dt { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.72; margin: 0; }
.bp-grid dd { font-size: 15px; font-weight: 700; margin: 2px 0 0; }
.bp-perf {
width: 2px;
background-image: linear-gradient(var(--surface) 50%, transparent 50%);
background-size: 2px 12px;
position: relative;
}
.bp-perf::before, .bp-perf::after {
content: ""; position: absolute; left: -9px;
width: 20px; height: 20px; border-radius: 50%;
background: var(--bg);
}
.bp-perf::before { top: -10px; }
.bp-perf::after { bottom: -10px; }
.bp-stub {
width: 150px; flex: 0 0 auto;
background: var(--surface);
padding: 22px 16px;
display: flex; flex-direction: column; gap: 14px;
}
.bp-stub-grid { display: grid; grid-template-columns: auto 1fr; gap: 6px 8px; }
.bp-stub-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); align-self: center; }
.bp-stub-val { font-size: 14px; font-weight: 800; color: var(--ink); text-align: right; }
.barcode {
height: 56px; margin-top: auto;
background-image: repeating-linear-gradient(90deg, var(--ink) 0 2px, transparent 2px 3px, var(--ink) 3px 4px, transparent 4px 7px);
border-radius: 3px;
}
.bp-seq { font-size: 10px; letter-spacing: 0.05em; color: var(--muted); text-align: center; }
.bp-actions { display: flex; gap: 12px; margin-top: 16px; }
.bp-actions .btn { flex: 1; }
/* Footer nav */
.navbar {
position: sticky; bottom: 0;
display: flex; align-items: center; gap: 12px;
padding: 14px 20px;
background: rgba(245, 248, 252, 0.9);
backdrop-filter: blur(8px);
border-top: 1px solid var(--line);
}
.nav-spacer { flex: 1; }
.navbar .btn { min-width: 130px; }
/* Toast */
.toast-wrap {
position: fixed; left: 50%; bottom: 88px;
transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px;
z-index: 50; pointer-events: none;
}
.toast {
background: var(--ink); color: #fff;
font-size: 13.5px; font-weight: 600;
padding: 11px 16px; border-radius: 999px;
box-shadow: var(--shadow-md);
display: flex; align-items: center; gap: 8px;
animation: toast-in .3s ease both;
}
.toast.out { animation: toast-out .3s ease forwards; }
.toast::before { content: "✓"; color: #8df0bb; font-weight: 800; }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(12px); } }
/* Responsive */
@media (max-width: 520px) {
.topbar { padding: 14px 16px; }
.stepper { padding: 4px 16px 8px; }
.step .label { display: none; }
.step.is-active .label { display: inline; }
.stage { padding: 10px 16px 16px; }
.card { padding: 16px; }
.form-grid, .contact-grid { grid-template-columns: 1fr; }
.endpoint .code { font-size: 24px; }
.bp-grid { grid-template-columns: repeat(2, 1fr); }
.boarding-pass { flex-direction: column; }
.bp-perf {
width: auto; height: 2px;
background-image: linear-gradient(90deg, var(--surface) 50%, transparent 50%);
background-size: 12px 2px;
}
.bp-perf::before, .bp-perf::after { top: -9px; left: auto; }
.bp-perf::before { left: -10px; }
.bp-perf::after { right: -10px; left: auto; bottom: auto; top: -9px; }
.bp-stub { width: auto; }
.barcode { margin-top: 14px; }
.navbar { padding: 12px 16px; }
.navbar .btn { min-width: 110px; flex: 1; }
}(function () {
"use strict";
// ---- State -------------------------------------------------------------
var BAG_PRICE = 45;
var TOTAL_STEPS = 6;
var booking = {
ref: "QX7P2K",
lastname: "Okafor",
passenger: "Chidi Okafor",
};
var state = {
step: 0,
seat: null,
seatFee: 0,
bags: 0,
docs: { passport: false, hazmat: false, health: false },
};
// ---- Helpers -----------------------------------------------------------
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function $$(sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); }
function toast(msg) {
var wrap = $("#toast-wrap");
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
wrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () { el.remove(); }, 320);
}, 2400);
}
var stepper = $("#stepper");
var backBtn = $("#back-btn");
var nextBtn = $("#next-btn");
// ---- Navigation --------------------------------------------------------
function panelFor(i) { return $('.panel[data-panel="' + i + '"]'); }
function showStep(i) {
state.step = i;
$$(".panel").forEach(function (p) {
var on = Number(p.getAttribute("data-panel")) === i;
p.classList.toggle("is-active", on);
p.hidden = !on;
});
$$(".step", stepper).forEach(function (s) {
var n = Number(s.getAttribute("data-step"));
s.classList.toggle("is-active", n === i);
s.classList.toggle("is-done", n < i);
});
backBtn.hidden = i === 0;
// Last step manages its own actions; hide footer continue.
if (i === TOTAL_STEPS - 1) {
nextBtn.style.display = "none";
backBtn.hidden = true;
} else {
nextBtn.style.display = "";
nextBtn.textContent = i === TOTAL_STEPS - 2 ? "Check in" : "Continue";
}
refreshNext();
window.scrollTo({ top: 0, behavior: "smooth" });
}
// Continue button enabled/disabled per step rules.
function refreshNext() {
var ok = true;
switch (state.step) {
case 0: ok = false; break; // advanced via find-form submit
case 4: ok = state.docs.passport && state.docs.hazmat && state.docs.health; break;
default: ok = true;
}
nextBtn.disabled = !ok;
}
backBtn.addEventListener("click", function () {
if (state.step > 0) showStep(state.step - 1);
});
nextBtn.addEventListener("click", function () {
if (state.step === 4 && !(state.docs.passport && state.docs.hazmat && state.docs.health)) {
$("#docs-warn").hidden = false;
return;
}
if (state.step === TOTAL_STEPS - 2) {
issueBoardingPass();
}
if (state.step < TOTAL_STEPS - 1) showStep(state.step + 1);
});
// Allow clicking a completed step in the stepper to jump back.
stepper.addEventListener("click", function (e) {
var li = e.target.closest(".step");
if (!li) return;
var n = Number(li.getAttribute("data-step"));
if (n < state.step) showStep(n);
});
// ---- Step 0: Find booking ---------------------------------------------
var findForm = $("#find-form");
var refInput = $("#ref");
var lastInput = $("#lastname");
$$("[data-fill]").forEach(function (btn) {
btn.addEventListener("click", function () {
var parts = btn.getAttribute("data-fill").split("|");
refInput.value = parts[0];
lastInput.value = parts[1];
refInput.classList.remove("invalid");
lastInput.classList.remove("invalid");
});
});
refInput.addEventListener("input", function () {
refInput.value = refInput.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6);
});
findForm.addEventListener("submit", function (e) {
e.preventDefault();
var ref = refInput.value.trim();
var last = lastInput.value.trim();
var bad = false;
if (ref.length !== 6) { refInput.classList.add("invalid"); bad = true; }
else { refInput.classList.remove("invalid"); }
if (last.length < 2) { lastInput.classList.add("invalid"); bad = true; }
else { lastInput.classList.remove("invalid"); }
if (bad) { toast("Check the reference and last name."); return; }
booking.ref = ref;
booking.lastname = last;
// Derive a plausible passenger name from the entered last name.
booking.passenger = "Chidi " + last.charAt(0).toUpperCase() + last.slice(1).toLowerCase();
$("#pax-ref").textContent = ref;
var nameEl = $(".pax-name");
if (nameEl) nameEl.textContent = booking.passenger;
var av = $(".avatar");
if (av) av.textContent = (booking.passenger.split(" ").map(function (w) { return w[0]; }).join("")).slice(0, 2).toUpperCase();
toast("Booking " + ref + " found.");
showStep(1);
});
// ---- Step 2: Seat map --------------------------------------------------
var cabin = $("#cabin");
var seatStatus = $("#seat-status");
var COLS = ["A", "B", "C", "D", "E", "F"];
var ROWS = 8;
var startRow = 12;
// Deterministic "taken" + "extra" layout.
var taken = { "12C": 1, "13A": 1, "13F": 1, "14B": 1, "15E": 1, "16C": 1, "17A": 1, "17D": 1, "18F": 1, "19B": 1 };
var extraRows = { 12: 1, 16: 1 }; // extra legroom rows
function buildCabin() {
for (var r = 0; r < ROWS; r++) {
var rowNum = startRow + r;
var row = document.createElement("div");
row.className = "seat-row";
var label = document.createElement("span");
label.className = "row-num";
label.textContent = rowNum;
row.appendChild(label);
COLS.forEach(function (col, idx) {
if (idx === 3) {
var gap = document.createElement("span");
gap.className = "aisle-gap";
gap.textContent = rowNum;
row.appendChild(gap);
}
var id = rowNum + col;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "seat";
btn.textContent = col;
btn.setAttribute("aria-label", "Seat " + id);
btn.dataset.seat = id;
var isExtra = extraRows[rowNum];
if (isExtra) { btn.classList.add("extra"); btn.dataset.fee = "39"; }
if (taken[id]) {
btn.classList.add("taken");
btn.disabled = true;
btn.setAttribute("aria-label", "Seat " + id + " unavailable");
} else {
btn.addEventListener("click", function () { selectSeat(btn); });
}
row.appendChild(btn);
});
cabin.appendChild(row);
}
}
function selectSeat(btn) {
$$(".seat.selected", cabin).forEach(function (s) { s.classList.remove("selected"); });
btn.classList.add("selected");
state.seat = btn.dataset.seat;
state.seatFee = btn.dataset.fee ? Number(btn.dataset.fee) : 0;
seatStatus.classList.add("chosen");
if (state.seatFee) {
seatStatus.textContent = "Seat " + state.seat + " selected — extra legroom, $" + state.seatFee + ".00.";
} else {
seatStatus.textContent = "Seat " + state.seat + " selected. Window/middle/aisle confirmed.";
}
toast("Seat " + state.seat + " selected.");
}
buildCabin();
// ---- Step 3: Bags ------------------------------------------------------
var bagMinus = $("#bag-minus");
var bagPlus = $("#bag-plus");
var bagCount = $("#bag-count");
var bagTotal = $("#bag-total");
function renderBags() {
bagCount.textContent = state.bags;
bagTotal.textContent = "$" + (state.bags * BAG_PRICE).toFixed(2);
bagMinus.disabled = state.bags === 0;
bagPlus.disabled = state.bags >= 5;
}
bagMinus.addEventListener("click", function () {
if (state.bags > 0) { state.bags--; renderBags(); }
});
bagPlus.addEventListener("click", function () {
if (state.bags < 5) { state.bags++; renderBags(); toast("Checked bag added · $" + BAG_PRICE + "."); }
});
renderBags();
// ---- Step 4: Documents -------------------------------------------------
$$('#check-list input[type="checkbox"]').forEach(function (cb) {
cb.addEventListener("change", function () {
state.docs[cb.dataset.doc] = cb.checked;
cb.closest(".check-item").classList.toggle("on", cb.checked);
var all = state.docs.passport && state.docs.hazmat && state.docs.health;
if (all) $("#docs-warn").hidden = true;
refreshNext();
});
});
// ---- Step 5: Boarding pass --------------------------------------------
function issueBoardingPass() {
var seat = state.seat || "Auto";
$("#bp-name").textContent = booking.passenger;
$("#bp-seat").textContent = seat;
$("#bp-stub-seat").textContent = seat;
// Boarding group from cabin position (front rows board later in this demo).
var group = state.seat ? String(((Number(state.seat.replace(/\D/g, "")) - 11) % 4) + 1) : "4";
$("#bp-group").textContent = group;
// Render a pseudo-random but stable barcode pattern.
renderBarcode($("#barcode"), booking.ref + seat);
toast("Boarding pass issued.");
}
function renderBarcode(el, seedStr) {
var seed = 0;
for (var i = 0; i < seedStr.length; i++) seed = (seed * 31 + seedStr.charCodeAt(i)) >>> 0;
var bars = [];
for (var b = 0; b < 48; b++) {
seed = (seed * 1103515245 + 12345) >>> 0;
bars.push((seed % 3) + 1);
}
var grad = [];
var x = 0;
bars.forEach(function (w, idx) {
var dark = idx % 2 === 0;
grad.push((dark ? "var(--ink)" : "transparent") + " " + x + "px " + (x + w) + "px");
x += w;
});
el.style.backgroundImage = "linear-gradient(90deg, " + grad.join(", ") + ")";
el.style.backgroundSize = x + "px 100%";
el.style.backgroundRepeat = "repeat-x";
}
$("#add-wallet").addEventListener("click", function () {
toast("Boarding pass saved to wallet.");
});
$("#restart").addEventListener("click", function () {
state.seat = null; state.seatFee = 0; state.bags = 0;
state.docs = { passport: false, hazmat: false, health: false };
$$(".seat.selected", cabin).forEach(function (s) { s.classList.remove("selected"); });
seatStatus.classList.remove("chosen");
seatStatus.textContent = "No seat selected — a seat will be assigned at the gate.";
$$('#check-list input[type="checkbox"]').forEach(function (cb) {
cb.checked = false; cb.closest(".check-item").classList.remove("on");
});
renderBags();
refInput.value = ""; lastInput.value = "";
showStep(0);
});
// ---- Init --------------------------------------------------------------
showStep(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyline Air — Online Check-in</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3.5S18 3 16.5 4.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/></svg>
</span>
<div class="brand-name">Skyline <strong>Air</strong></div>
</div>
<div class="topbar-meta">
<span class="badge-secure" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Secure check-in
</span>
</div>
</header>
<!-- Stepper -->
<nav class="stepper" aria-label="Check-in progress">
<ol id="stepper">
<li class="step is-active" data-step="0"><span class="dot">1</span><span class="label">Find booking</span></li>
<li class="step" data-step="1"><span class="dot">2</span><span class="label">Passenger</span></li>
<li class="step" data-step="2"><span class="dot">3</span><span class="label">Seat</span></li>
<li class="step" data-step="3"><span class="dot">4</span><span class="label">Bags</span></li>
<li class="step" data-step="4"><span class="dot">5</span><span class="label">Documents</span></li>
<li class="step" data-step="5"><span class="dot">6</span><span class="label">Boarding pass</span></li>
</ol>
</nav>
<main class="stage">
<!-- STEP 0: Find booking -->
<section class="panel is-active" data-panel="0" aria-labelledby="h-find">
<div class="card flight-summary">
<div class="route">
<div class="endpoint">
<span class="code">JFK</span>
<span class="city">New York</span>
<span class="time">08:45</span>
</div>
<div class="leg" aria-hidden="true">
<span class="leg-line"></span>
<svg class="leg-plane" viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z"/></svg>
<span class="leg-line"></span>
</div>
<div class="endpoint align-end">
<span class="code">LHR</span>
<span class="city">London</span>
<span class="time">20:30</span>
</div>
</div>
<div class="flight-meta">
<span><strong>SK 218</strong> · Boeing 787-9</span>
<span class="pill pill-ok">On time</span>
<span>Wed 18 Jun · 7h 45m</span>
</div>
</div>
<div class="card">
<h1 id="h-find" class="card-title">Find your booking</h1>
<p class="card-sub">Enter your 6-character booking reference and the last name on the reservation.</p>
<form id="find-form" class="form-grid" novalidate>
<div class="field">
<label for="ref">Booking reference</label>
<input id="ref" name="ref" inputmode="text" autocomplete="off" maxlength="6" placeholder="e.g. QX7P2K" aria-describedby="ref-hint" />
<small id="ref-hint" class="hint">6 letters & numbers — found in your confirmation email.</small>
</div>
<div class="field">
<label for="lastname">Last name</label>
<input id="lastname" name="lastname" autocomplete="family-name" placeholder="e.g. Okafor" />
</div>
<div class="field field-full">
<button type="submit" class="btn btn-primary btn-block">Find booking</button>
</div>
<p class="prefill-note">Demo refs: <button type="button" class="linkbtn" data-fill="QX7P2K|Okafor">QX7P2K</button> · <button type="button" class="linkbtn" data-fill="MD4L9N|Tanaka">MD4L9N</button></p>
</form>
</div>
</section>
<!-- STEP 1: Passenger -->
<section class="panel" data-panel="1" aria-labelledby="h-pax" hidden>
<div class="card">
<h2 id="h-pax" class="card-title">Confirm passenger</h2>
<p class="card-sub">Booking <strong id="pax-ref">QX7P2K</strong> · 1 passenger · Economy</p>
<ul class="pax-list" id="pax-list">
<li class="pax-row">
<span class="avatar" aria-hidden="true">CO</span>
<div class="pax-info">
<span class="pax-name">Chidi Okafor</span>
<span class="pax-detail">Adult · Frequent flyer SkyMiles ·· 4821</span>
</div>
<span class="pill pill-muted">Not checked in</span>
</li>
</ul>
<div class="contact-grid">
<div class="field">
<label for="email">Contact email</label>
<input id="email" type="email" value="c.okafor@example.com" autocomplete="email" />
</div>
<div class="field">
<label for="phone">Mobile (for alerts)</label>
<input id="phone" type="tel" value="+44 7700 900118" autocomplete="tel" />
</div>
</div>
</div>
</section>
<!-- STEP 2: Seat -->
<section class="panel" data-panel="2" aria-labelledby="h-seat" hidden>
<div class="card">
<h2 id="h-seat" class="card-title">Choose your seat</h2>
<p class="card-sub">Tap a seat to select. Exit-row and front seats carry a small fee.</p>
<div class="seat-legend">
<span><i class="sw sw-free"></i> Available</span>
<span><i class="sw sw-extra"></i> Extra legroom · $39</span>
<span><i class="sw sw-taken"></i> Taken</span>
<span><i class="sw sw-sel"></i> Selected</span>
</div>
<div class="cabin" id="cabin" role="grid" aria-label="Seat map">
<div class="seat-cols" aria-hidden="true">
<span>A</span><span>B</span><span>C</span><span class="aisle"></span><span>D</span><span>E</span><span>F</span>
</div>
<!-- rows injected by JS -->
</div>
<div class="seat-status" id="seat-status">No seat selected — a seat will be assigned at the gate.</div>
</div>
</section>
<!-- STEP 3: Bags -->
<section class="panel" data-panel="3" aria-labelledby="h-bags" hidden>
<div class="card">
<h2 id="h-bags" class="card-title">Checked bags</h2>
<p class="card-sub">1 cabin bag (up to 8 kg) is included. Add checked bags below.</p>
<div class="bag-counter">
<div class="bag-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="7" width="14" height="14" rx="2"/><path d="M9 7V5a3 3 0 0 1 6 0v2M9 11v6M15 11v6"/></svg>
</div>
<div class="bag-copy">
<span class="bag-title">Checked bag · 23 kg</span>
<span class="bag-price">$45 each</span>
</div>
<div class="stepper-ctrl" role="group" aria-label="Number of checked bags">
<button type="button" class="rounder" id="bag-minus" aria-label="Remove a bag">−</button>
<output id="bag-count" class="bag-count">0</output>
<button type="button" class="rounder" id="bag-plus" aria-label="Add a bag">+</button>
</div>
</div>
<div class="total-row">
<span>Bag total</span>
<span id="bag-total" class="total-amount">$0.00</span>
</div>
</div>
</section>
<!-- STEP 4: Documents -->
<section class="panel" data-panel="4" aria-labelledby="h-docs" hidden>
<div class="card">
<h2 id="h-docs" class="card-title">Travel documents & declarations</h2>
<p class="card-sub">Confirm the items below before we can issue your boarding pass.</p>
<ul class="check-list" id="check-list">
<li class="check-item">
<label class="switch">
<input type="checkbox" data-doc="passport" />
<span class="track" aria-hidden="true"></span>
</label>
<div class="check-copy">
<span class="check-title">Passport details verified</span>
<span class="check-detail">Passport ·· 6F29 expires 03/2031 — valid for travel.</span>
</div>
</li>
<li class="check-item">
<label class="switch">
<input type="checkbox" data-doc="hazmat" />
<span class="track" aria-hidden="true"></span>
</label>
<div class="check-copy">
<span class="check-title">No restricted items in baggage</span>
<span class="check-detail">I am not carrying prohibited or dangerous goods.</span>
</div>
</li>
<li class="check-item">
<label class="switch">
<input type="checkbox" data-doc="health" />
<span class="track" aria-hidden="true"></span>
</label>
<div class="check-copy">
<span class="check-title">Health declaration</span>
<span class="check-detail">I meet the entry health requirements for the United Kingdom.</span>
</div>
</li>
</ul>
<p class="docs-warn" id="docs-warn" hidden>Please confirm all three declarations to continue.</p>
</div>
</section>
<!-- STEP 5: Boarding pass -->
<section class="panel" data-panel="5" aria-labelledby="h-bp" hidden>
<div class="bp-intro">
<h2 id="h-bp" class="card-title">You're checked in</h2>
<p class="card-sub">Boarding pass issued — present it at security and the gate.</p>
</div>
<div class="boarding-pass" id="boarding-pass">
<div class="bp-main">
<div class="bp-head">
<span class="bp-brand">Skyline Air</span>
<span class="pill pill-board">Boarding 19:45</span>
</div>
<div class="bp-route">
<div class="bp-end">
<span class="bp-code">JFK</span>
<span class="bp-city">New York</span>
</div>
<svg class="bp-plane" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5z"/></svg>
<div class="bp-end align-end">
<span class="bp-code">LHR</span>
<span class="bp-city">London</span>
</div>
</div>
<dl class="bp-grid">
<div><dt>Passenger</dt><dd id="bp-name">Chidi Okafor</dd></div>
<div><dt>Flight</dt><dd>SK 218</dd></div>
<div><dt>Date</dt><dd>18 Jun</dd></div>
<div><dt>Boarding</dt><dd>19:45</dd></div>
<div><dt>Gate</dt><dd id="bp-gate">B22</dd></div>
<div><dt>Seat</dt><dd id="bp-seat">—</dd></div>
<div><dt>Group</dt><dd id="bp-group">4</dd></div>
<div><dt>Terminal</dt><dd>7</dd></div>
</dl>
</div>
<div class="bp-perf" aria-hidden="true"></div>
<div class="bp-stub">
<div class="bp-stub-grid">
<span class="bp-stub-label">Seat</span><span class="bp-stub-val" id="bp-stub-seat">—</span>
<span class="bp-stub-label">Gate</span><span class="bp-stub-val" id="bp-stub-gate">B22</span>
<span class="bp-stub-label">Flight</span><span class="bp-stub-val">SK 218</span>
</div>
<div class="barcode" id="barcode" aria-label="Boarding pass barcode"></div>
<span class="bp-seq">SEQ 042 · SK218JFKLHR</span>
</div>
</div>
<div class="bp-actions">
<button type="button" class="btn btn-ghost" id="add-wallet">Add to wallet</button>
<button type="button" class="btn btn-primary" id="restart">New check-in</button>
</div>
</section>
</main>
<!-- Footer nav -->
<footer class="navbar" id="navbar">
<button type="button" class="btn btn-ghost" id="back-btn" hidden>Back</button>
<div class="nav-spacer"></div>
<button type="button" class="btn btn-primary" id="next-btn" disabled>Continue</button>
</footer>
</div>
<div class="toast-wrap" id="toast-wrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Online Check-in
A self-contained online check-in flow for the fictional Skyline Air, modelled on the precise, status-forward feel of real airline apps. A pill-based stepper tracks progress across six stages — find booking, passenger, seat, bags, documents, and boarding pass — and completed steps stay clickable so travellers can jump back without losing their selections. The aviation blue and sunrise-orange palette, airport codes (JFK → LHR), 24-hour times, and tabular figures keep flight data crisp and readable down to a 360px phone screen.
The wizard is genuinely interactive. Entering a 6-character reference and last name validates the input and reveals the passenger card; the seat map renders a real cabin grid with an aisle gap, taken seats, and priced extra-legroom rows that update a live status line. A bag stepper totals checked-baggage fees, and three declaration toggles gate the final step until all are confirmed.
Issuing the boarding pass animates a card with a dashed perforation, a stub showing seat and gate, a procedurally generated barcode seeded from the booking, and a boarding group derived from the chosen row. Toasts confirm each action, and a single reset returns the flow to a clean start.
Illustrative UI only — fictional airline, not a real booking or flight system.