Airline — Self-Check-in Kiosk
A touch-first airport self-check-in kiosk UI for a fictional carrier, Aurelia Air. Retrieve a booking by document scan, reference, or frequent flyer, then move through a five-step flow: select passengers, pick a seat from an interactive cabin map with extra-legroom rows, add checked bags with a live fee summary, and watch the boarding pass and bag tags print with a perforated stub, barcode, and success animation. Large targets, status pills, and tabular figures throughout.
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: 0 1px 2px rgba(19, 35, 59, 0.06), 0 12px 30px rgba(19, 35, 59, 0.08);
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.08);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 22px;
display: flex;
justify-content: center;
min-height: 100vh;
}
.tnum { font-variant-numeric: tabular-nums; }
.kiosk {
width: 100%;
max-width: 760px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
display: flex;
flex-direction: column;
align-self: flex-start;
}
/* ---------- top bar ---------- */
.kiosk__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 22px;
background: linear-gradient(120deg, var(--sky-d), var(--sky));
color: #fff;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
width: 40px; height: 40px;
display: grid; place-items: center;
border-radius: 12px;
background: rgba(255, 255, 255, 0.16);
}
.brand__name {
font-weight: 800; font-size: 17px; letter-spacing: -0.01em;
display: flex; flex-direction: column; line-height: 1.15;
}
.brand__name small { font-weight: 500; font-size: 11px; opacity: 0.8; letter-spacing: 0.04em; text-transform: uppercase; }
.kiosk__meta { display: flex; align-items: center; gap: 9px; font-size: 12.5px; }
.kiosk__status { opacity: 0.9; font-weight: 500; }
.kiosk__clock {
font-variant-numeric: tabular-nums;
font-weight: 700; font-size: 15px;
background: rgba(255, 255, 255, 0.16);
padding: 4px 10px; border-radius: 8px;
}
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot--ok { background: #7dffb6; box-shadow: 0 0 0 4px rgba(125, 255, 182, 0.25); }
/* ---------- steps ---------- */
.steps {
display: flex; gap: 4px;
list-style: none; margin: 0;
padding: 14px 22px;
background: var(--cloud);
border-bottom: 1px solid var(--line);
counter-reset: step;
overflow-x: auto;
}
.steps__item {
flex: 1 1 0; min-width: 84px;
display: flex; align-items: center; gap: 8px;
font-size: 12px; font-weight: 600; color: var(--muted);
position: relative;
}
.steps__item::before {
counter-increment: step;
content: attr(data-step-label);
width: 22px; height: 22px; flex: none;
display: grid; place-items: center;
border-radius: 50%;
background: #fff; border: 1.5px solid var(--line-2);
font-size: 11px; color: var(--muted);
transition: 0.2s;
}
.steps__item.is-active { color: var(--ink); }
.steps__item.is-active::before { background: var(--sky); border-color: var(--sky); color: #fff; }
.steps__item.is-done { color: var(--ink-2); }
.steps__item.is-done::before {
background: var(--ok); border-color: var(--ok); color: #fff;
content: "✓";
}
.steps__item span { white-space: nowrap; }
/* ---------- stage / panels ---------- */
.stage { padding: 26px 24px 28px; }
.panel { display: none; animation: rise 0.32s ease both; }
.panel.is-active { display: block; }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.panel__title { margin: 0 0 5px; font-size: 25px; font-weight: 800; letter-spacing: -0.02em; }
.panel__sub { margin: 0 0 18px; color: var(--muted); font-size: 14.5px; }
.panel__sub strong { color: var(--ink-2); font-variant-numeric: tabular-nums; }
/* ---------- scan ---------- */
.scan {
width: 100%;
display: flex; align-items: center; gap: 16px;
padding: 20px;
border: 2px dashed var(--line-2);
border-radius: var(--r-md);
background: var(--sky-50);
color: var(--sky-d);
cursor: pointer; text-align: left;
font-family: inherit;
transition: 0.18s;
}
.scan:hover { border-color: var(--sky); background: #def; }
.scan:active { transform: scale(0.99); }
.scan.is-scanning { border-style: solid; border-color: var(--sky); background: #fff; }
.scan__icon {
width: 54px; height: 54px; flex: none;
display: grid; place-items: center;
background: #fff; border-radius: 12px; color: var(--sky);
box-shadow: var(--shadow-sm);
position: relative; overflow: hidden;
}
.scan.is-scanning .scan__icon::after {
content: ""; position: absolute; left: 0; right: 0; height: 3px;
background: var(--sunrise); box-shadow: 0 0 8px var(--sunrise);
animation: scanline 0.9s linear infinite;
}
@keyframes scanline { 0% { top: 6px; } 100% { top: 48px; } }
.scan__txt strong { display: block; font-size: 16px; }
.scan__txt small { color: var(--muted); font-size: 12.5px; }
.divider { display: flex; align-items: center; gap: 12px; color: var(--muted); font-size: 12px; margin: 18px 0; }
.divider::before, .divider::after { content: ""; height: 1px; flex: 1; background: var(--line); }
.retrieve { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field__label { font-size: 12.5px; font-weight: 600; color: var(--ink-2); }
.field__input {
font-family: inherit; font-size: 17px; font-weight: 600;
padding: 14px 14px; border: 1.5px solid var(--line-2);
border-radius: var(--r-sm); background: #fff; color: var(--ink);
letter-spacing: 0.02em; transition: 0.15s; width: 100%;
}
#refInput { text-transform: uppercase; font-variant-numeric: tabular-nums; }
.field__input:focus { outline: none; border-color: var(--sky); box-shadow: 0 0 0 3px var(--sky-50); }
.field__input.is-err { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(212, 73, 62, 0.12); }
.hint { font-size: 12.5px; color: var(--muted); margin: 10px 0 2px; }
.hint strong { color: var(--ink-2); }
.hint__demo {
font: inherit; font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums;
color: var(--sky); background: var(--sky-50); border: none;
padding: 1px 7px; border-radius: 6px; cursor: pointer;
}
.hint__demo:hover { background: #d4e7fa; }
.ff {
margin-top: 6px; background: none; border: none; cursor: pointer;
font: inherit; font-size: 13px; font-weight: 600; color: var(--sky);
padding: 6px 0; text-decoration: underline; text-underline-offset: 3px;
}
/* ---------- buttons ---------- */
.actions { margin-top: 22px; }
.actions--split { display: flex; gap: 12px; }
.actions--split .btn--primary { flex: 1; }
.btn {
font-family: inherit; font-weight: 700; font-size: 15px;
border: none; border-radius: var(--r-sm); cursor: pointer;
padding: 14px 22px; transition: 0.16s;
}
.btn--lg { width: 100%; font-size: 17px; padding: 17px; }
.btn--primary { background: var(--sky); color: #fff; box-shadow: 0 6px 16px rgba(10, 102, 194, 0.28); }
.btn--primary:hover { background: var(--sky-d); }
.btn--primary:active { transform: translateY(1px); }
.btn--primary:disabled { background: #b6c6da; box-shadow: none; cursor: not-allowed; }
.btn--ghost { background: #fff; color: var(--ink-2); border: 1.5px solid var(--line-2); }
.btn--ghost:hover { border-color: var(--ink-2); background: var(--cloud); }
/* ---------- flight card ---------- */
.flightcard {
border: 1px solid var(--line); border-radius: var(--r-md);
background: linear-gradient(180deg, #fff, var(--cloud));
padding: 16px 18px; margin-bottom: 18px; box-shadow: var(--shadow-sm);
}
.flightcard__route { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.airport__code { font-size: 26px; font-weight: 800; letter-spacing: -0.01em; }
.airport__city { font-size: 12.5px; color: var(--muted); }
.airport__time { font-size: 14px; font-weight: 700; color: var(--ink-2); font-variant-numeric: tabular-nums; display: block; margin-top: 2px; }
.airport--end { text-align: right; }
.flightcard__mid { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; min-width: 0; }
.flightcard__no { font-size: 12px; font-weight: 700; color: var(--sky-d); background: var(--sky-50); padding: 2px 9px; border-radius: 6px; font-variant-numeric: tabular-nums; }
.path { display: flex; align-items: center; gap: 4px; color: var(--sky); width: 100%; max-width: 150px; justify-content: center; }
.path i { height: 2px; flex: 1; background: repeating-linear-gradient(90deg, var(--line-2) 0 5px, transparent 5px 9px); }
.flightcard__dur { font-size: 11px; color: var(--muted); }
.flightcard__foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.metaline { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 700; letter-spacing: 0.01em;
padding: 5px 11px; border-radius: 999px; white-space: nowrap;
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill--boarding { color: var(--boarding); background: rgba(31, 157, 98, 0.12); }
.pill--ontime { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.pill--delayed { color: var(--warn); background: rgba(224, 150, 42, 0.14); }
/* ---------- passenger list ---------- */
.paxlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.pax {
display: flex; align-items: center; gap: 14px;
padding: 14px 16px; border: 1.5px solid var(--line-2);
border-radius: var(--r-md); background: #fff; cursor: pointer;
transition: 0.15s; text-align: left; width: 100%; font-family: inherit;
}
.pax:hover { border-color: var(--sky); background: var(--sky-50); }
.pax.is-on { border-color: var(--sky); background: var(--sky-50); box-shadow: inset 0 0 0 1px var(--sky); }
.pax__check {
width: 26px; height: 26px; flex: none; border-radius: 8px;
border: 2px solid var(--line-2); display: grid; place-items: center;
color: #fff; transition: 0.15s;
}
.pax.is-on .pax__check { background: var(--sky); border-color: var(--sky); }
.pax__check svg { opacity: 0; transition: 0.15s; }
.pax.is-on .pax__check svg { opacity: 1; }
.pax__avatar {
width: 40px; height: 40px; flex: none; border-radius: 50%;
display: grid; place-items: center; font-weight: 800; font-size: 15px;
background: var(--sunrise-50); color: var(--sunrise);
}
.pax__info { flex: 1; min-width: 0; }
.pax__name { font-weight: 700; font-size: 15.5px; }
.pax__sub { font-size: 12.5px; color: var(--muted); }
.pax__tag { font-size: 11px; font-weight: 700; color: var(--sky-d); background: var(--sky-50); padding: 2px 8px; border-radius: 6px; }
/* ---------- seat map ---------- */
.seatwrap { display: grid; grid-template-columns: 1fr 190px; gap: 16px; align-items: start; }
.cabin {
border: 1px solid var(--line); border-radius: var(--r-md);
background: var(--cloud); padding: 16px; box-shadow: var(--shadow-sm);
}
.cabin__nose { text-align: center; font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px dashed var(--line-2); }
.seatmap { display: flex; flex-direction: column; gap: 8px; }
.seatrow { display: flex; align-items: center; gap: 7px; justify-content: center; }
.seatrow__no { width: 22px; text-align: right; font-size: 11px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.aisle { width: 16px; }
.seat {
width: 34px; height: 34px; border-radius: 8px 8px 10px 10px;
border: 1.5px solid var(--line-2); background: #fff; cursor: pointer;
font-size: 10px; font-weight: 700; color: var(--ink-2);
display: grid; place-items: center; transition: 0.14s; font-family: inherit;
}
.seat:hover:not(.seat--taken) { transform: translateY(-2px); border-color: var(--sky); }
.seat--open { color: var(--sky-d); border-color: #b6d3f0; }
.seat--extra { background: var(--sunrise-50); border-color: #ffc9a8; color: var(--sunrise); }
.seat--taken { background: #e7ecf3; border-color: #e7ecf3; color: #aab7c8; cursor: not-allowed; }
.seat.is-sel { background: var(--sky); border-color: var(--sky-d); color: #fff; box-shadow: 0 4px 10px rgba(10, 102, 194, 0.4); transform: translateY(-2px); }
.cabin__legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--ink-2); }
.cabin__legend span { display: flex; align-items: center; gap: 5px; }
.sw { width: 13px; height: 13px; border-radius: 4px; display: inline-block; border: 1.5px solid var(--line-2); }
.sw--open { background: #fff; border-color: #b6d3f0; }
.sw--extra { background: var(--sunrise-50); border-color: #ffc9a8; }
.sw--taken { background: #e7ecf3; border-color: #e7ecf3; }
.sw--sel { background: var(--sky); border-color: var(--sky-d); }
.seatpick {
border: 1px solid var(--line); border-radius: var(--r-md);
background: linear-gradient(180deg, var(--sky-50), #fff);
padding: 18px 16px; text-align: center; box-shadow: var(--shadow-sm);
position: sticky; top: 14px;
}
.seatpick__big { font-size: 40px; font-weight: 800; color: var(--sky-d); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.seatpick__desc { font-size: 13px; color: var(--ink-2); font-weight: 600; min-height: 20px; }
.seatpick__fee { margin-top: 10px; font-size: 13px; font-weight: 700; color: var(--sunrise); min-height: 20px; }
/* ---------- bags ---------- */
.bags { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--cloud); padding: 26px 20px; text-align: center; box-shadow: var(--shadow-sm); }
.bags__icon { color: var(--sky); display: flex; justify-content: center; margin-bottom: 10px; }
.stepper { display: inline-flex; align-items: center; gap: 18px; }
.stepper__btn {
width: 56px; height: 56px; border-radius: 14px;
border: 1.5px solid var(--line-2); background: #fff; color: var(--sky-d);
font-size: 28px; font-weight: 700; cursor: pointer; transition: 0.14s; line-height: 1;
}
.stepper__btn:hover { border-color: var(--sky); background: var(--sky-50); }
.stepper__btn:active { transform: scale(0.94); }
.stepper__btn:disabled { opacity: 0.4; cursor: not-allowed; }
.stepper__val { min-width: 70px; }
.stepper__val span { font-size: 38px; font-weight: 800; font-variant-numeric: tabular-nums; display: block; line-height: 1; }
.stepper__val small { font-size: 12px; color: var(--muted); font-weight: 600; }
.bags__note { font-size: 12.5px; color: var(--muted); margin: 14px 0 0; }
.summary { margin-top: 16px; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.summary__row { display: flex; justify-content: space-between; padding: 11px 16px; font-size: 14px; border-bottom: 1px solid var(--line); }
.summary__row:last-child { border-bottom: none; }
.summary__row span { color: var(--muted); }
.summary__row strong { font-variant-numeric: tabular-nums; }
.summary__row--total { background: var(--cloud); font-weight: 800; }
.summary__row--total span { color: var(--ink); }
/* ---------- printer / boarding pass ---------- */
.panel--done { text-align: center; }
.printer { max-width: 460px; margin: 0 auto 20px; }
.printer__head {
display: flex; align-items: center; justify-content: center; gap: 9px;
font-size: 13px; font-weight: 600; color: var(--ink-2);
background: #1a2942; color: #cdd8e8;
padding: 12px; border-radius: 14px 14px 0 0;
}
.printer__led { width: 9px; height: 9px; border-radius: 50%; background: var(--warn); box-shadow: 0 0 8px var(--warn); animation: blink 0.7s infinite; }
.printer.is-done .printer__led { background: var(--ok); box-shadow: 0 0 8px var(--ok); animation: none; }
@keyframes blink { 50% { opacity: 0.3; } }
.printer__slot {
background: #13233b; padding: 0 14px;
height: 0; overflow: hidden;
transition: height 1.4s cubic-bezier(0.2, 0.8, 0.2, 1), padding 1.4s;
border-radius: 0 0 14px 14px;
}
.printer.is-printing .printer__slot, .printer.is-done .printer__slot { height: 318px; padding: 14px; }
.bpass {
background: #fff; border-radius: var(--r-md); overflow: hidden;
display: flex; text-align: left; box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35);
}
.bpass__main { flex: 1; padding: 16px; min-width: 0; }
.bpass__brand { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 15px; color: var(--sky-d); flex-wrap: wrap; }
.bpass__logo { color: var(--sunrise); }
.bpass__pill { margin-left: auto; font-size: 10.5px; padding: 3px 8px; }
.bpass__route { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 14px 0; color: var(--sky); }
.bpass__code { font-size: 24px; font-weight: 800; color: var(--ink); letter-spacing: -0.01em; }
.bpass__city { font-size: 11px; color: var(--muted); }
.ta-r { text-align: right; }
.bpass__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 8px; }
.bpass__grid span { display: block; font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.bpass__grid strong { font-size: 14px; font-variant-numeric: tabular-nums; }
.bpass__barcode { height: 34px; margin-top: 14px; background: repeating-linear-gradient(90deg, #13233b 0 2px, #fff 2px 4px, #13233b 4px 7px, #fff 7px 9px, #13233b 9px 11px, #fff 11px 16px); }
.bpass__stub {
width: 118px; flex: none; padding: 16px 12px; background: var(--cloud);
border-left: 2px dashed var(--line-2);
position: relative;
}
.bpass__stub::before, .bpass__stub::after {
content: ""; position: absolute; left: -9px; width: 16px; height: 16px;
background: #13233b; border-radius: 50%;
}
.bpass__stub::before { top: -8px; }
.bpass__stub::after { bottom: -8px; }
.stub__row { margin-bottom: 10px; }
.stub__row span { display: block; font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.stub__row strong { font-size: 15px; font-variant-numeric: tabular-nums; }
.stub__barcode { height: 60px; margin: 12px 0 6px; background: repeating-linear-gradient(90deg, #13233b 0 2px, transparent 2px 4px, #13233b 4px 7px, transparent 7px 10px); }
.stub__no { font-size: 10px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.done__check {
width: 60px; height: 60px; margin: 0 auto 14px;
border-radius: 50%; background: var(--ok); color: #fff;
display: grid; place-items: center;
transform: scale(0); transition: transform 0.4s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.printer.is-done ~ .done .done__check { transform: scale(1); }
.done__title { margin: 0 0 5px; font-size: 22px; font-weight: 800; }
.done__sub { margin: 0 auto 18px; color: var(--muted); font-size: 14px; max-width: 380px; }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 24px);
background: #13233b; color: #fff; font-size: 13.5px; font-weight: 600;
padding: 12px 20px; border-radius: 999px; box-shadow: 0 12px 30px rgba(0, 0, 0, 0.28);
opacity: 0; pointer-events: none; transition: 0.3s; z-index: 50; max-width: 90vw;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body { padding: 0; }
.kiosk { border-radius: 0; min-height: 100vh; }
.steps__item span { display: none; }
.steps__item { flex: none; min-width: 0; }
.stage { padding: 20px 16px 24px; }
.panel__title { font-size: 21px; }
.retrieve { grid-template-columns: 1fr; }
.seatwrap { grid-template-columns: 1fr; }
.seatpick { position: static; }
.seat { width: 30px; height: 30px; }
.bpass__stub { width: 96px; }
.kiosk__status { display: none; }
}(function () {
"use strict";
// ---------- fictional booking data ----------
var BOOKING = {
ref: "7QK2RX",
lastName: "MARSH",
flight: "AU 418",
passengers: [
{ id: "p1", first: "Elena", last: "Marsh", initials: "EM", type: "Adult", tier: "Gold" },
{ id: "p2", first: "Theo", last: "Marsh", initials: "TM", type: "Adult", tier: null },
{ id: "p3", first: "Iris", last: "Marsh", initials: "IM", type: "Child (8)", tier: null }
]
};
var SEAT_FEE_EXTRA = 45;
var BAG_FEE = 60;
var GROUPS = ["A1", "B2", "C1"];
// taken seats + extra-legroom rows
var TAKEN = ["12A", "12C", "13F", "15B", "15C", "16D", "17A", "17F", "18E"];
var EXTRA_ROWS = [11];
var ROWS = [11, 12, 13, 14, 15, 16, 17, 18];
var COLS = ["A", "B", "C", "D", "E", "F"];
var state = { passengers: [], seat: null, bags: 1 };
// ---------- helpers ----------
function $(s, r) { return (r || document).querySelector(s); }
function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
var toastEl = $("#toast");
var toastT;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastT);
toastT = setTimeout(function () { toastEl.classList.remove("is-show"); }, 2600);
}
function money(n) { return "$" + n.toFixed(0); }
// ---------- clock ----------
(function clock() {
var el = $("#clock");
function tick() {
var d = new Date();
el.textContent =
String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
}
tick();
setInterval(tick, 15000);
})();
// ---------- step navigation ----------
var current = 1;
function goTo(step) {
current = step;
$$(".panel").forEach(function (p) {
var s = Number(p.getAttribute("data-step"));
var on = s === step;
p.classList.toggle("is-active", on);
p.hidden = !on;
});
$$(".steps__item").forEach(function (li, i) {
var n = i + 1;
li.classList.toggle("is-active", n === step);
li.classList.toggle("is-done", n < step);
});
var k = $(".kiosk");
if (k && k.scrollIntoView) k.scrollIntoView({ block: "start", behavior: "smooth" });
}
$$("[data-back]").forEach(function (b) {
b.addEventListener("click", function () { goTo(Number(b.getAttribute("data-back"))); });
});
// ---------- STEP 1: retrieve ----------
var refInput = $("#refInput");
var nameInput = $("#nameInput");
$("#demoRef").addEventListener("click", function () {
refInput.value = BOOKING.ref;
nameInput.value = "Marsh";
refInput.classList.remove("is-err");
toast("Demo booking filled in");
});
$("#scanBtn").addEventListener("click", function () {
var btn = this;
if (btn.classList.contains("is-scanning")) return;
btn.classList.add("is-scanning");
toast("Reading document…");
setTimeout(function () {
btn.classList.remove("is-scanning");
refInput.value = BOOKING.ref;
nameInput.value = "Marsh";
toast("Booking " + BOOKING.ref + " found");
foundBooking();
}, 1300);
});
$("#ffBtn").addEventListener("click", function () {
toast("Frequent flyer lookup is offline at this kiosk — use reference instead");
});
$("#findBtn").addEventListener("click", function () {
var ref = refInput.value.trim().toUpperCase();
var name = nameInput.value.trim().toUpperCase();
if (ref !== BOOKING.ref || name !== BOOKING.lastName) {
refInput.classList.add("is-err");
nameInput.classList.toggle("is-err", name !== BOOKING.lastName);
toast("No booking matches those details. Try " + BOOKING.ref + " / Marsh");
return;
}
refInput.classList.remove("is-err");
nameInput.classList.remove("is-err");
foundBooking();
});
refInput.addEventListener("input", function () { this.classList.remove("is-err"); });
nameInput.addEventListener("input", function () { this.classList.remove("is-err"); });
function foundBooking() {
$("#bookingRef2").textContent = BOOKING.ref;
buildPaxList();
goTo(2);
toast("Welcome aboard, Marsh party");
}
// ---------- STEP 2: passengers ----------
var paxNext = $("#paxNext");
function buildPaxList() {
var ul = $("#paxList");
ul.innerHTML = "";
BOOKING.passengers.forEach(function (p) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "pax";
btn.setAttribute("aria-pressed", "false");
btn.dataset.id = p.id;
btn.innerHTML =
'<span class="pax__check"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></span>' +
'<span class="pax__avatar">' + p.initials + "</span>" +
'<span class="pax__info"><span class="pax__name">' + p.first + " " + p.last + "</span>" +
'<span class="pax__sub">' + p.type + " · Economy</span></span>" +
(p.tier ? '<span class="pax__tag">' + p.tier + "</span>" : "");
btn.addEventListener("click", function () {
var on = btn.classList.toggle("is-on");
btn.setAttribute("aria-pressed", String(on));
updatePaxSel();
});
li.appendChild(btn);
ul.appendChild(li);
});
// preselect all
$$(".pax").forEach(function (b) { b.classList.add("is-on"); b.setAttribute("aria-pressed", "true"); });
updatePaxSel();
}
function updatePaxSel() {
state.passengers = $$(".pax.is-on").map(function (b) { return b.dataset.id; });
paxNext.disabled = state.passengers.length === 0;
paxNext.textContent =
state.passengers.length > 1
? "Continue · " + state.passengers.length + " passengers"
: "Continue";
}
paxNext.addEventListener("click", function () {
var first = BOOKING.passengers.filter(function (p) {
return state.passengers.indexOf(p.id) !== -1;
})[0];
$("#seatPaxName").textContent = first ? first.first + " " + first.last : "—";
buildSeatMap();
goTo(3);
});
// ---------- STEP 3: seats ----------
var seatNext = $("#seatNext");
function buildSeatMap() {
var map = $("#seatMap");
map.innerHTML = "";
ROWS.forEach(function (row) {
var rowEl = document.createElement("div");
rowEl.className = "seatrow";
rowEl.setAttribute("role", "row");
var no = document.createElement("span");
no.className = "seatrow__no";
no.textContent = row;
rowEl.appendChild(no);
COLS.forEach(function (col, ci) {
if (ci === 3) {
var aisle = document.createElement("span");
aisle.className = "aisle";
rowEl.appendChild(aisle);
}
var id = row + col;
var seat = document.createElement("button");
seat.type = "button";
seat.className = "seat";
seat.textContent = col;
seat.dataset.seat = id;
seat.setAttribute("role", "gridcell");
var isExtra = EXTRA_ROWS.indexOf(row) !== -1;
if (TAKEN.indexOf(id) !== -1) {
seat.classList.add("seat--taken");
seat.disabled = true;
seat.setAttribute("aria-label", "Seat " + id + " taken");
} else {
seat.classList.add(isExtra ? "seat--extra" : "seat--open");
seat.dataset.extra = isExtra ? "1" : "0";
seat.setAttribute("aria-label", "Seat " + id + (isExtra ? ", extra legroom" : ""));
seat.addEventListener("click", function () { pickSeat(seat); });
}
rowEl.appendChild(seat);
});
map.appendChild(rowEl);
});
}
function pickSeat(seat) {
$$(".seat.is-sel").forEach(function (s) { s.classList.remove("is-sel"); });
seat.classList.add("is-sel");
var id = seat.dataset.seat;
var extra = seat.dataset.extra === "1";
state.seat = { id: id, extra: extra };
$("#seatBig").textContent = id;
var col = id.slice(-1);
var pos = col === "A" || col === "F" ? "Window" : col === "C" || col === "D" ? "Aisle" : "Middle";
$("#seatDesc").textContent = pos + " seat" + (extra ? " · extra legroom" : "");
$("#seatFee").textContent = extra ? "+" + money(SEAT_FEE_EXTRA) + " seat fee" : "No extra charge";
seatNext.disabled = false;
}
seatNext.addEventListener("click", function () {
renderBagSummary();
goTo(4);
});
// ---------- STEP 4: bags ----------
var bagCountEl = $("#bagCount");
var bagMinus = $("#bagMinus");
var bagPlus = $("#bagPlus");
function setBags(n) {
state.bags = Math.max(0, Math.min(5, n));
bagCountEl.textContent = state.bags;
bagMinus.disabled = state.bags === 0;
bagPlus.disabled = state.bags === 5;
var extra = Math.max(0, state.bags - 1);
$("#bagNote").textContent =
state.bags === 0
? "Travelling with carry-on only"
: "First bag included" + (extra ? " · " + extra + " extra at " + money(BAG_FEE) + " each" : "") + " · 23 kg limit";
renderBagSummary();
}
bagMinus.addEventListener("click", function () { setBags(state.bags - 1); });
bagPlus.addEventListener("click", function () { setBags(state.bags + 1); });
function renderBagSummary() {
var extraBags = Math.max(0, state.bags - 1) * BAG_FEE;
var seatFee = state.seat && state.seat.extra ? SEAT_FEE_EXTRA : 0;
var total = extraBags + seatFee;
var rows = "";
rows += row("Checked bags", state.bags + " × " + (state.bags ? "23 kg" : "—"), state.bags > 1 ? money(extraBags) : "Included");
if (state.seat) rows += row("Seat " + state.seat.id, state.seat.extra ? "Extra legroom" : "Standard", seatFee ? money(seatFee) : "Free");
rows +=
'<div class="summary__row summary__row--total"><span>Total due</span><strong>' +
(total ? money(total) : "$0") + "</strong></div>";
$("#bagSummary").innerHTML = rows;
function row(label, sub, val) {
return (
'<div class="summary__row"><span>' + label + " · " + sub +
"</span><strong>" + val + "</strong></div>"
);
}
}
$("#bagNext").addEventListener("click", function () {
fillBoardingPass();
goTo(5);
runPrint();
});
// ---------- STEP 5: boarding pass + print ----------
function fillBoardingPass() {
var pax = BOOKING.passengers.filter(function (p) {
return state.passengers.indexOf(p.id) !== -1;
})[0] || BOOKING.passengers[0];
var name = (pax.last + "/" + pax.first).toUpperCase();
$("#bpName").textContent = name;
var seat = state.seat ? state.seat.id : "14C";
$("#bpSeat").textContent = seat;
$("#bpSeat2").textContent = seat;
$("#bpBags").textContent = state.bags;
var grp = pax.tier === "Gold" ? "A1" : GROUPS[(Number(seat.slice(0, -1)) % GROUPS.length)] || "C1";
$("#bpGroup").textContent = grp.slice(-2);
$("#bpSeq").textContent = "SEQ 0" + (40 + Math.floor(Math.random() * 9));
}
function runPrint() {
var printer = $("#printer");
var msg = $("#printerMsg");
var title = $("#doneTitle");
var sub = $("#doneSub");
printer.classList.remove("is-done");
printer.classList.add("is-printing");
msg.textContent = "Printing boarding pass & bag tags…";
title.textContent = "Printing…";
setTimeout(function () {
printer.classList.remove("is-printing");
printer.classList.add("is-done");
msg.textContent = "Boarding pass & " + state.bags + " bag tag" + (state.bags === 1 ? "" : "s") + " ready";
title.textContent = "You're checked in!";
sub.textContent =
"Collect your documents below. Drop bags at the Aurelia Air counter, then proceed to Gate B22.";
toast("Check-in complete — Gate B22 boards 06:00");
}, 1700);
}
$("#restartBtn").addEventListener("click", function () {
state = { passengers: [], seat: null, bags: 1 };
refInput.value = "";
nameInput.value = "";
$("#printer").classList.remove("is-printing", "is-done");
$("#seatBig").textContent = "—";
$("#seatDesc").textContent = "No seat selected";
$("#seatFee").textContent = "";
seatNext.disabled = true;
setBags(1);
goTo(1);
toast("Ready for the next passenger");
});
// init defaults
setBags(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurelia Air — Self-Check-in Kiosk</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="kiosk" role="application" aria-label="Self check-in kiosk">
<header class="kiosk__top">
<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.5a2.1 2.1 0 0 0-3-3L13 8 4.8 6.2a1 1 0 0 0-.9 1.7l4.3 3-1.6 3.5-2.4-.5a1 1 0 0 0-.9 1.7l3 2 2 3a1 1 0 0 0 1.7-.9l-.5-2.4 3.5-1.6 3 4.3a1 1 0 0 0 1.7-.9z"/></svg>
</span>
<div class="brand__name">Aurelia Air<small>Self-Check-in</small></div>
</div>
<div class="kiosk__meta">
<span class="dot dot--ok" aria-hidden="true"></span>
<span class="kiosk__status">Kiosk T2-14 · Online</span>
<time id="clock" class="kiosk__clock" aria-label="Current time">06:42</time>
</div>
</header>
<ol class="steps" aria-label="Progress">
<li class="steps__item is-active" data-step-label="1"><span>Find booking</span></li>
<li class="steps__item" data-step-label="2"><span>Passengers</span></li>
<li class="steps__item" data-step-label="3"><span>Seat</span></li>
<li class="steps__item" data-step-label="4"><span>Bags</span></li>
<li class="steps__item" data-step-label="5"><span>Boarding pass</span></li>
</ol>
<main class="stage">
<!-- STEP 1 — RETRIEVE BOOKING -->
<section class="panel is-active" data-step="1" aria-label="Find your booking">
<h1 class="panel__title">Find your booking</h1>
<p class="panel__sub">Scan your passport or boarding pass, or enter your details below.</p>
<button class="scan" type="button" id="scanBtn">
<span class="scan__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
</span>
<span class="scan__txt"><strong>Scan document</strong><small>Tap and hold under the reader</small></span>
</button>
<div class="divider"><span>or enter manually</span></div>
<div class="retrieve">
<label class="field">
<span class="field__label">Booking reference</span>
<input id="refInput" class="field__input" type="text" inputmode="text" autocomplete="off"
placeholder="e.g. 7QK2RX" maxlength="6" aria-describedby="refHint" />
</label>
<label class="field">
<span class="field__label">Last name</span>
<input id="nameInput" class="field__input" type="text" autocomplete="off" placeholder="e.g. Marsh" />
</label>
</div>
<p id="refHint" class="hint">Try reference <button type="button" class="hint__demo" id="demoRef">7QK2RX</button> · last name <strong>Marsh</strong></p>
<button class="ff" type="button" id="ffBtn">
Use frequent flyer number instead
</button>
<div class="actions">
<button class="btn btn--lg btn--primary" type="button" id="findBtn">Find booking</button>
</div>
</section>
<!-- STEP 2 — PASSENGERS -->
<section class="panel" data-step="2" aria-label="Select passengers" hidden>
<h1 class="panel__title">Who is checking in?</h1>
<p class="panel__sub">Booking <strong id="bookingRef2">7QK2RX</strong> · select the passengers to check in.</p>
<div class="flightcard" id="flightCard">
<div class="flightcard__route">
<div class="airport">
<div class="airport__code">JFK</div>
<div class="airport__city">New York</div>
<time class="airport__time">06:42</time>
</div>
<div class="flightcard__mid">
<span class="flightcard__no">AU 418</span>
<span class="path" aria-hidden="true"><i></i><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M22 12 13 5v4L3 11l-1 1 1 1 10 2v4z"/></svg><i></i></span>
<span class="flightcard__dur">7h 05m · Nonstop</span>
</div>
<div class="airport airport--end">
<div class="airport__code">LHR</div>
<div class="airport__city">London</div>
<time class="airport__time">19:47</time>
</div>
</div>
<div class="flightcard__foot">
<span class="pill pill--boarding">Boarding 06:00</span>
<span class="metaline">Gate B22 · Terminal 2 · 18 Jun</span>
</div>
</div>
<ul class="paxlist" id="paxList" role="group" aria-label="Passengers"></ul>
<div class="actions actions--split">
<button class="btn btn--ghost" type="button" data-back="1">Back</button>
<button class="btn btn--primary" type="button" id="paxNext" disabled>Continue</button>
</div>
</section>
<!-- STEP 3 — SEAT -->
<section class="panel" data-step="3" aria-label="Choose a seat" hidden>
<h1 class="panel__title">Choose your seat</h1>
<p class="panel__sub">Selecting for <strong id="seatPaxName">—</strong> · tap an open seat.</p>
<div class="seatwrap">
<div class="cabin">
<div class="cabin__nose">Front of aircraft</div>
<div class="seatmap" id="seatMap" role="grid" aria-label="Seat map"></div>
<div class="cabin__legend">
<span><i class="sw sw--open"></i>Available</span>
<span><i class="sw sw--extra"></i>Extra legroom (+$45)</span>
<span><i class="sw sw--taken"></i>Taken</span>
<span><i class="sw sw--sel"></i>Selected</span>
</div>
</div>
<aside class="seatpick" aria-live="polite">
<div class="seatpick__big" id="seatBig">—</div>
<div class="seatpick__desc" id="seatDesc">No seat selected</div>
<div class="seatpick__fee" id="seatFee"></div>
</aside>
</div>
<div class="actions actions--split">
<button class="btn btn--ghost" type="button" data-back="2">Back</button>
<button class="btn btn--primary" type="button" id="seatNext" disabled>Confirm seat</button>
</div>
</section>
<!-- STEP 4 — BAGS -->
<section class="panel" data-step="4" aria-label="Add bags" hidden>
<h1 class="panel__title">Checked bags</h1>
<p class="panel__sub">1 bag included with your fare. Extra bags are $60 each.</p>
<div class="bags">
<div class="bags__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="7" width="14" height="13" rx="2"/><path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2M9 20v1M15 20v1"/></svg>
</div>
<div class="stepper" role="group" aria-label="Number of checked bags">
<button class="stepper__btn" type="button" id="bagMinus" aria-label="Remove a bag">−</button>
<div class="stepper__val"><span id="bagCount">1</span><small>bags</small></div>
<button class="stepper__btn" type="button" id="bagPlus" aria-label="Add a bag">+</button>
</div>
<p class="bags__note" id="bagNote">First bag included · weight limit 23 kg each</p>
</div>
<div class="summary" id="bagSummary"></div>
<div class="actions actions--split">
<button class="btn btn--ghost" type="button" data-back="3">Back</button>
<button class="btn btn--primary" type="button" id="bagNext">Check in & print</button>
</div>
</section>
<!-- STEP 5 — PRINT / BOARDING PASS -->
<section class="panel panel--done" data-step="5" aria-label="Boarding pass" hidden>
<div class="printer" id="printer">
<div class="printer__head">
<span class="printer__led" id="printerLed"></span>
<span id="printerMsg">Printing boarding pass & bag tags…</span>
</div>
<div class="printer__slot">
<div class="bpass" id="bpass">
<div class="bpass__main">
<div class="bpass__brand">
<span class="bpass__logo" aria-hidden="true">✦</span> Aurelia Air
<span class="pill pill--boarding bpass__pill">Boarding 06:00</span>
</div>
<div class="bpass__route">
<div><div class="bpass__code">JFK</div><div class="bpass__city">New York</div></div>
<svg class="bpass__plane" viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12 13 5v4L3 11l-1 1 1 1 10 2v4z"/></svg>
<div class="ta-r"><div class="bpass__code">LHR</div><div class="bpass__city">London</div></div>
</div>
<div class="bpass__grid">
<div><span>Passenger</span><strong id="bpName">—</strong></div>
<div><span>Flight</span><strong>AU 418</strong></div>
<div><span>Date</span><strong>18 Jun</strong></div>
<div><span>Boarding</span><strong>06:00</strong></div>
<div><span>Gate</span><strong>B22</strong></div>
<div><span>Seat</span><strong id="bpSeat">—</strong></div>
<div><span>Terminal</span><strong>2</strong></div>
<div><span>Group</span><strong id="bpGroup">3</strong></div>
</div>
<div class="bpass__barcode" aria-hidden="true"></div>
</div>
<div class="bpass__stub">
<div class="stub__row"><span>Seat</span><strong id="bpSeat2">—</strong></div>
<div class="stub__row"><span>Flight</span><strong>AU 418</strong></div>
<div class="stub__row"><span>Bags</span><strong id="bpBags">1</strong></div>
<div class="stub__barcode" aria-hidden="true"></div>
<div class="stub__no" id="bpSeq">SEQ 042</div>
</div>
</div>
</div>
</div>
<div class="done">
<div class="done__check" id="doneCheck" aria-hidden="true">
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
</div>
<h2 class="done__title" id="doneTitle">Printing…</h2>
<p class="done__sub" id="doneSub">Please collect your boarding pass and bag tags below the screen.</p>
<div class="actions">
<button class="btn btn--lg btn--primary" type="button" id="restartBtn">Done — start over</button>
</div>
</div>
</section>
</main>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Self-Check-in Kiosk
A full-screen self-service check-in kiosk for the fictional Aurelia Air, built mobile-first with oversized touch targets and a clear five-step progress rail: find booking, passengers, seat, bags, and boarding pass. Step one lets travellers scan a document (a simulated reader sweep auto-fills the demo booking), type a reference and last name, or fall back to a frequent flyer lookup. A live clock and kiosk status sit in the aviation-blue header.
Once the booking 7QK2RX / Marsh is found, a flight card shows the JFK→LHR route with 24h times, flight number AU 418, gate, terminal, and a green Boarding pill. Passengers are selectable cards with avatars and tier tags; the seat step renders an interactive cabin grid where taken seats are locked, extra-legroom rows carry a +$45 fee, and the live picker reports window/aisle/middle position. The bag stepper recalculates a running total of seat and bag charges.
Checking in triggers a printer animation: the slot extends, a barcode-and-perforation boarding pass with a tear-off stub slides out, the LED turns green, and a check-mark pops with the gate reminder. Everything is vanilla JS with a small toast() helper, and a Done button resets the kiosk for the next passenger.
Illustrative UI only — fictional airline, not a real booking or flight system.