Airline — Seat Map Selector
An interactive aircraft seat-map selector for the fictional Skyhaven Air A350-900, spanning first, business, and economy cabins. Passengers tap open seats to select them, with a color-coded legend for available, occupied, extra-legroom, and exit-row seats. Hovering reveals a tooltip with the seat number and price, a running summary tallies the live total, a per-passenger maximum guards selection, and a deck-zoom control scales the cabin for precise tapping on small screens.
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 8px 24px rgba(19, 35, 59, 0.08);
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.08);
font-variant-numeric: tabular-nums;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
max-width: 1080px;
margin: 0 auto;
padding: 24px 20px 56px;
}
/* ---------- Topbar ---------- */
.topbar {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 20px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.route { display: flex; align-items: center; gap: 18px; }
.endpoint { display: flex; flex-direction: column; }
.endpoint .code {
font-size: 30px;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.endpoint .city { font-size: 13px; color: var(--muted); font-weight: 500; }
.leg { display: flex; align-items: center; gap: 6px; color: var(--sky); }
.leg .line {
width: 30px; height: 2px;
background: repeating-linear-gradient(90deg, var(--line-2) 0 4px, transparent 4px 8px);
}
.leg .plane { transform: rotate(0deg); }
.flight-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
.meta-grid {
display: grid;
grid-template-columns: repeat(4, auto);
gap: 18px;
}
.meta-grid > div { display: flex; flex-direction: column; text-align: right; }
.meta-grid .k { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 600; }
.meta-grid .v { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
.pill {
display: inline-flex; align-items: center; gap: 7px;
font-size: 13px; font-weight: 700;
padding: 6px 14px; border-radius: 999px;
}
.pill .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 22%, transparent); }
.pill-boarding { background: color-mix(in srgb, var(--boarding) 14%, white); color: var(--boarding); }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
margin-top: 20px;
align-items: start;
}
/* ---------- Deck ---------- */
.deck-panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 18px 18px 22px;
}
.deck-toolbar {
display: flex; align-items: center; justify-content: space-between;
gap: 14px; flex-wrap: wrap; margin-bottom: 16px;
}
.legend { display: flex; gap: 14px; flex-wrap: wrap; }
.lg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--ink-2); }
.swatch { width: 14px; height: 14px; border-radius: 4px; display: inline-block; border: 1px solid var(--line-2); }
.s-avail { background: var(--surface); }
.s-legroom { background: var(--sky-50); border-color: color-mix(in srgb, var(--sky) 40%, white); }
.s-exit { background: var(--sunrise-50); border-color: color-mix(in srgb, var(--sunrise) 45%, white); }
.s-occupied { background: #d9e0ea; border-color: #c2ccd9; }
.s-selected { background: var(--sky); border-color: var(--sky-d); }
.zoom { display: inline-flex; align-items: center; gap: 8px; }
.zbtn {
width: 32px; height: 32px; border-radius: var(--r-sm);
border: 1px solid var(--line-2); background: var(--surface);
font-size: 18px; font-weight: 700; color: var(--ink); cursor: pointer;
display: grid; place-items: center; transition: background 0.15s, border-color 0.15s;
}
.zbtn:hover { background: var(--sky-50); border-color: var(--sky); }
.zbtn:active { transform: scale(0.94); }
.zbtn:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.zlabel { font-size: 12px; font-weight: 700; color: var(--muted); min-width: 40px; text-align: center; font-variant-numeric: tabular-nums; }
.deck-scroll { overflow: auto; padding: 6px; }
.fuselage {
position: relative;
margin: 0 auto;
width: max-content;
transform-origin: top center;
transition: transform 0.2s ease;
padding: 4px;
}
.nose, .tail {
width: 100%; height: 40px; margin: 0 auto;
background: linear-gradient(var(--cloud), #fff);
border: 1px solid var(--line);
}
.nose { border-radius: 80px 80px 12px 12px; border-bottom: none; }
.tail { border-radius: 12px 12px 60px 60px; border-top: none; }
.cabin {
border-left: 1px solid var(--line);
border-right: 1px solid var(--line);
padding: 8px 14px;
background:
linear-gradient(#fff, #fff) padding-box,
repeating-linear-gradient(#fff 0 0);
}
.zone-label {
display: flex; align-items: center; gap: 10px;
font-size: 11px; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--sky); margin: 14px 2px 8px;
}
.zone-label::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.zone-label.first { color: var(--sunrise); }
.row {
display: grid;
grid-template-columns: 22px repeat(var(--cols, 9), 30px);
gap: 6px;
align-items: center;
justify-content: center;
margin-bottom: 6px;
}
.row.exit-row { position: relative; }
.row.exit-row::before, .row.exit-row::after {
content: "EXIT"; position: absolute; top: 50%; transform: translateY(-50%);
font-size: 8px; font-weight: 800; letter-spacing: 0.08em;
color: #fff; background: var(--sunrise);
padding: 2px 4px; border-radius: 3px;
}
.row.exit-row::before { left: -40px; }
.row.exit-row::after { right: -40px; }
.row-num { font-size: 11px; font-weight: 700; color: var(--muted); text-align: center; font-variant-numeric: tabular-nums; }
.aisle-gap { width: 12px; }
.seat {
width: 30px; height: 30px;
border-radius: 7px 7px 5px 5px;
border: 1px solid var(--line-2);
background: var(--surface);
font-size: 9px; font-weight: 700; color: var(--ink-2);
cursor: pointer;
display: grid; place-items: center;
position: relative;
transition: transform 0.12s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
font-variant-numeric: tabular-nums;
}
.seat::after {
content: ""; position: absolute; top: 2px; left: 4px; right: 4px; height: 6px;
border-radius: 3px 3px 0 0; background: rgba(19, 35, 59, 0.07);
}
.seat:hover:not(.occupied) { transform: translateY(-2px); box-shadow: var(--shadow-sm); border-color: var(--sky); }
.seat:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.seat.legroom { background: var(--sky-50); border-color: color-mix(in srgb, var(--sky) 35%, white); }
.seat.exit { background: var(--sunrise-50); border-color: color-mix(in srgb, var(--sunrise) 45%, white); }
.seat.first { border-radius: 8px; }
.seat.occupied {
background: #d9e0ea; border-color: #c2ccd9; color: #9aa6b6; cursor: not-allowed;
}
.seat.occupied::after { background: rgba(19, 35, 59, 0.05); }
.seat.selected {
background: var(--sky); border-color: var(--sky-d); color: #fff;
box-shadow: 0 4px 12px rgba(10, 102, 194, 0.35);
}
.seat.selected::after { background: rgba(255, 255, 255, 0.25); }
.seat.selected::before {
content: "✓"; position: absolute; font-size: 13px; font-weight: 800;
}
.col-key {
display: grid;
grid-template-columns: 22px repeat(9, 30px);
gap: 6px;
justify-content: center;
margin-top: 8px;
font-size: 10px; font-weight: 700; color: var(--muted);
}
.col-key span { text-align: center; }
.col-key .aisle { width: 12px; }
/* ---------- Summary ---------- */
.summary {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 20px;
position: sticky;
top: 16px;
}
.pax-card {
background: var(--cloud);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
margin-bottom: 18px;
}
.pax-card .k { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 600; }
.pax-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.avatar {
width: 38px; height: 38px; border-radius: 50%;
background: var(--sky); color: #fff;
display: grid; place-items: center; font-size: 13px; font-weight: 700; flex-shrink: 0;
}
.pax-row strong { display: block; font-size: 14px; }
.pax-row .muted { font-size: 12px; color: var(--muted); }
.muted { color: var(--muted); }
.sum-title { font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-2); margin: 0 0 10px; }
.seat-list { list-style: none; margin: 0 0 16px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.seat-list .empty { font-size: 13px; color: var(--muted); padding: 10px 0; }
.seat-item {
display: flex; align-items: center; justify-content: space-between;
gap: 10px; padding: 10px 12px;
border: 1px solid var(--line); border-radius: var(--r-md);
animation: pop 0.18s ease;
}
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.seat-item .badge {
width: 40px; height: 34px; border-radius: var(--r-sm);
background: var(--sky); color: #fff;
display: grid; place-items: center; font-size: 12px; font-weight: 800;
flex-shrink: 0; font-variant-numeric: tabular-nums;
}
.seat-item .info { flex: 1; min-width: 0; }
.seat-item .info strong { font-size: 13px; }
.seat-item .info span { display: block; font-size: 11px; color: var(--muted); }
.seat-item .price { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
.seat-item .rm {
border: none; background: none; cursor: pointer; color: var(--muted);
font-size: 18px; line-height: 1; padding: 2px 4px; border-radius: 6px;
}
.seat-item .rm:hover { color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, white); }
.seat-item .rm:focus-visible { outline: 2px solid var(--danger); outline-offset: 1px; }
.totals { margin: 0 0 16px; padding-top: 14px; border-top: 1px dashed var(--line-2); }
.t-row { display: flex; justify-content: space-between; align-items: baseline; padding: 4px 0; font-size: 14px; }
.t-row dt { color: var(--muted); }
.t-row dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.t-row.total { margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--line); }
.t-row.total dt, .t-row.total dd { font-size: 18px; font-weight: 800; color: var(--ink); }
.cta {
width: 100%; padding: 13px; border: none; border-radius: var(--r-md);
background: var(--sky); color: #fff; font-size: 15px; font-weight: 700;
font-family: inherit; cursor: pointer; transition: background 0.15s, transform 0.1s;
}
.cta:hover:not(:disabled) { background: var(--sky-d); }
.cta:active:not(:disabled) { transform: scale(0.99); }
.cta:disabled { background: #c5d2e0; cursor: not-allowed; }
.cta:focus-visible { outline: 2px solid var(--sky-d); outline-offset: 2px; }
.fineprint { font-size: 11px; color: var(--muted); text-align: center; margin: 10px 0 0; }
/* ---------- Tooltip ---------- */
.tooltip {
position: fixed;
z-index: 50;
background: var(--ink);
color: #fff;
padding: 8px 11px;
border-radius: var(--r-sm);
font-size: 12px;
font-weight: 600;
pointer-events: none;
box-shadow: 0 6px 20px rgba(19, 35, 59, 0.28);
white-space: nowrap;
transform: translate(-50%, -118%);
transition: opacity 0.12s;
}
.tooltip .tt-num { font-weight: 800; font-size: 13px; }
.tooltip .tt-meta { color: #b8c6db; font-weight: 500; }
.tooltip .tt-price { color: var(--sunrise); }
.tooltip::after {
content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
border: 5px solid transparent; border-top-color: var(--ink);
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; z-index: 60; align-items: center;
}
.toast {
background: var(--ink); color: #fff;
padding: 11px 18px; border-radius: var(--r-md);
font-size: 13px; font-weight: 600;
box-shadow: 0 8px 24px rgba(19, 35, 59, 0.3);
animation: toastIn 0.22s ease;
}
.toast.warn { background: var(--warn); }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.layout { grid-template-columns: 1fr; }
.summary { position: static; }
}
@media (max-width: 520px) {
.app { padding: 14px 12px 40px; }
.topbar { padding: 16px; gap: 16px; }
.endpoint .code { font-size: 24px; }
.flight-meta { align-items: flex-start; width: 100%; }
.meta-grid { grid-template-columns: repeat(2, 1fr); gap: 12px 18px; width: 100%; }
.meta-grid > div { text-align: left; }
.legend { gap: 10px 12px; }
.lg { font-size: 11px; }
.deck-scroll { padding: 2px; }
}(function () {
"use strict";
var MAX_SEATS = 2;
var COLS = ["A", "B", "C", "D", "E", "F", "G", "K"];
// Zone definitions for the A350-900 (fictional Skyhaven config).
var ZONES = [
{
name: "First",
cls: "first",
cols: ["A", "C", "D", "G"], // 1-2-1 suite layout
aisleAfter: ["A", "D"],
rows: [1, 2, 3],
basePrice: 0,
kind: "first",
label: "First Class · Suite",
},
{
name: "Business",
cls: "biz",
cols: ["A", "C", "D", "G"], // 1-2-1
aisleAfter: ["A", "D"],
rows: [4, 5, 6, 7],
basePrice: 0,
kind: "business",
label: "Business · Lie-flat",
},
{
name: "Economy",
cls: "eco",
cols: ["A", "B", "C", "D", "E", "G", "K"], // 3-2-... use 7 across for demo
aisleAfter: ["C", "E"],
rows: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
basePrice: 14,
kind: "economy",
label: "Economy",
exitRows: [24],
legroomRows: [20],
},
];
// Deterministic pseudo-random occupancy + pricing.
function hash(str) {
var h = 2166136261;
for (var i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = (h * 16777619) >>> 0;
}
return h;
}
var seats = {}; // id -> seat data
var selected = []; // array of seat ids (preserves order)
var cabin = document.getElementById("cabin");
var seatList = document.getElementById("seat-list");
var emptyState = document.getElementById("empty-state");
var countOut = document.getElementById("count-out");
var subtotalOut = document.getElementById("subtotal-out");
var totalOut = document.getElementById("total-out");
var confirmBtn = document.getElementById("confirm-btn");
var tooltip = document.getElementById("tooltip");
var fuselage = document.getElementById("fuselage");
function money(n) {
return "$" + n.toFixed(2);
}
function seatTypeMeta(zone, row) {
var isExit = zone.exitRows && zone.exitRows.indexOf(row) !== -1;
var isLegroom = zone.legroomRows && zone.legroomRows.indexOf(row) !== -1;
var price = zone.basePrice;
var type = zone.label;
if (zone.kind === "first") {
price = 0;
type = "First suite (included)";
} else if (zone.kind === "business") {
price = 0;
type = "Lie-flat (included)";
} else {
if (isExit) {
price += 35;
type = "Exit row · extra legroom";
} else if (isLegroom) {
price += 22;
type = "Bulkhead · extra legroom";
} else {
type = "Standard economy";
}
}
return { price: price, type: type, isExit: isExit, isLegroom: isLegroom };
}
function build() {
ZONES.forEach(function (zone) {
var label = document.createElement("div");
label.className = "zone-label" + (zone.kind === "first" ? " first" : "");
label.textContent = zone.name;
cabin.appendChild(label);
var nCols = zone.cols.length;
zone.rows.forEach(function (row) {
var isExit = zone.exitRows && zone.exitRows.indexOf(row) !== -1;
var rowEl = document.createElement("div");
rowEl.className = "row" + (isExit ? " exit-row" : "");
rowEl.style.setProperty("--cols", String(nCols));
var num = document.createElement("span");
num.className = "row-num";
num.textContent = row;
rowEl.appendChild(num);
zone.cols.forEach(function (col) {
var id = row + col;
var meta = seatTypeMeta(zone, row);
var occupied = hash(id + zone.name) % 100 < 32; // ~32% taken
seats[id] = {
id: id,
row: row,
col: col,
zone: zone.name,
price: meta.price,
type: meta.type,
occupied: occupied,
};
var btn = document.createElement("button");
btn.type = "button";
btn.className = "seat " + zone.cls;
if (zone.kind === "first" || zone.kind === "business") btn.classList.add("first");
if (meta.isLegroom) btn.classList.add("legroom");
if (meta.isExit) btn.classList.add("exit");
if (occupied) btn.classList.add("occupied");
btn.textContent = col;
btn.dataset.id = id;
btn.setAttribute(
"aria-label",
"Seat " + id + ", " + meta.type +
(occupied ? ", occupied" : ", " + (meta.price === 0 ? "included" : money(meta.price)))
);
if (occupied) btn.setAttribute("aria-disabled", "true");
rowEl.appendChild(btn);
// aisle spacer
if (zone.aisleAfter.indexOf(col) !== -1) {
var gap = document.createElement("span");
gap.className = "aisle-gap";
gap.setAttribute("aria-hidden", "true");
rowEl.appendChild(gap);
}
});
cabin.appendChild(rowEl);
});
});
}
function toast(msg, kind) {
var wrap = document.getElementById("toast-wrap");
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.textContent = msg;
wrap.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(8px)";
setTimeout(function () { el.remove(); }, 320);
}, 2400);
}
function toggleSeat(id) {
var seat = seats[id];
if (!seat || seat.occupied) return;
var idx = selected.indexOf(id);
var btn = cabin.querySelector('.seat[data-id="' + id + '"]');
if (idx !== -1) {
selected.splice(idx, 1);
btn.classList.remove("selected");
btn.setAttribute("aria-pressed", "false");
} else {
if (selected.length >= MAX_SEATS) {
toast("You can pick up to " + MAX_SEATS + " seats for this passenger.", "warn");
return;
}
selected.push(id);
btn.classList.add("selected");
btn.setAttribute("aria-pressed", "true");
toast("Seat " + id + " added — " + (seat.price === 0 ? "included" : money(seat.price)));
}
render();
}
function render() {
// list
seatList.innerHTML = "";
if (selected.length === 0) {
seatList.appendChild(emptyState);
} else {
selected.forEach(function (id) {
var seat = seats[id];
var li = document.createElement("li");
li.className = "seat-item";
li.innerHTML =
'<span class="badge">' + seat.id + "</span>" +
'<span class="info"><strong>' + seat.zone + "</strong><span>" + seat.type + "</span></span>" +
'<span class="price">' + (seat.price === 0 ? "Incl." : money(seat.price)) + "</span>" +
'<button class="rm" type="button" aria-label="Remove seat ' + seat.id + '">×</button>';
li.querySelector(".rm").addEventListener("click", function () {
toggleSeat(id);
});
seatList.appendChild(li);
});
}
var subtotal = selected.reduce(function (sum, id) {
return sum + seats[id].price;
}, 0);
countOut.textContent = selected.length;
subtotalOut.textContent = money(subtotal);
totalOut.textContent = money(subtotal);
confirmBtn.disabled = selected.length === 0;
confirmBtn.textContent =
selected.length === 0
? "Confirm selection"
: "Confirm " + selected.length + " seat" + (selected.length > 1 ? "s" : "") + " · " + money(subtotal);
}
// ---- Tooltip ----
function showTooltip(seat, target) {
var rect = target.getBoundingClientRect();
var priceStr = seat.occupied
? '<span class="tt-meta">Occupied</span>'
: seat.price === 0
? '<span class="tt-price">Included</span>'
: '<span class="tt-price">' + money(seat.price) + "</span>";
tooltip.innerHTML =
'<span class="tt-num">' + seat.id + "</span> · " +
'<span class="tt-meta">' + seat.type + "</span><br>" + priceStr;
tooltip.hidden = false;
tooltip.style.left = rect.left + rect.width / 2 + "px";
tooltip.style.top = rect.top + "px";
}
function hideTooltip() { tooltip.hidden = true; }
// ---- Events (delegated) ----
cabin.addEventListener("click", function (e) {
var btn = e.target.closest(".seat");
if (btn && btn.dataset.id) toggleSeat(btn.dataset.id);
});
cabin.addEventListener("mouseover", function (e) {
var btn = e.target.closest(".seat");
if (btn && btn.dataset.id) showTooltip(seats[btn.dataset.id], btn);
});
cabin.addEventListener("mouseout", function (e) {
if (e.target.closest(".seat")) hideTooltip();
});
cabin.addEventListener("focusin", function (e) {
var btn = e.target.closest(".seat");
if (btn && btn.dataset.id) showTooltip(seats[btn.dataset.id], btn);
});
cabin.addEventListener("focusout", hideTooltip);
// ---- Zoom ----
var zoom = 1;
var ZMIN = 0.8, ZMAX = 1.4, ZSTEP = 0.1;
var zoomLabel = document.getElementById("zoom-label");
function applyZoom() {
fuselage.style.transform = "scale(" + zoom + ")";
zoomLabel.textContent = Math.round(zoom * 100) + "%";
document.getElementById("zoom-out").disabled = zoom <= ZMIN + 0.001;
document.getElementById("zoom-in").disabled = zoom >= ZMAX - 0.001;
}
document.getElementById("zoom-in").addEventListener("click", function () {
zoom = Math.min(ZMAX, +(zoom + ZSTEP).toFixed(2));
applyZoom();
});
document.getElementById("zoom-out").addEventListener("click", function () {
zoom = Math.max(ZMIN, +(zoom - ZSTEP).toFixed(2));
applyZoom();
});
confirmBtn.addEventListener("click", function () {
if (selected.length === 0) return;
toast("Seats " + selected.join(", ") + " confirmed for Amara Reyes.");
});
build();
applyZoom();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyhaven Air — Seat Map Selector</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>
<main class="app" role="main">
<header class="topbar">
<div class="route">
<div class="endpoint">
<span class="code">JFK</span>
<span class="city">New York</span>
</div>
<div class="leg" aria-hidden="true">
<span class="line"></span>
<svg class="plane" viewBox="0 0 24 24" width="22" height="22"><path d="M2 12l7-1 4-7 1.6.4-2 6.6 6.3-.7 2-1.6.6 1.5-7 3.4 1.4 4.4-1.5.6-3-3.4-6 1.6-1.8-1.5z" fill="currentColor"/></svg>
<span class="line"></span>
</div>
<div class="endpoint">
<span class="code">LHR</span>
<span class="city">London</span>
</div>
</div>
<div class="flight-meta">
<span class="pill pill-boarding" id="status-pill">
<span class="dot"></span> Boarding
</span>
<div class="meta-grid">
<div><span class="k">Flight</span><span class="v">SH 218</span></div>
<div><span class="k">Aircraft</span><span class="v">A350-900</span></div>
<div><span class="k">Gate</span><span class="v">B12</span></div>
<div><span class="k">Boards</span><span class="v">18:40</span></div>
</div>
</div>
</header>
<div class="layout">
<section class="deck-panel" aria-label="Aircraft seat map">
<div class="deck-toolbar">
<div class="legend" aria-label="Seat legend">
<span class="lg"><i class="swatch s-avail"></i>Available</span>
<span class="lg"><i class="swatch s-legroom"></i>Extra legroom</span>
<span class="lg"><i class="swatch s-exit"></i>Exit row</span>
<span class="lg"><i class="swatch s-occupied"></i>Occupied</span>
<span class="lg"><i class="swatch s-selected"></i>Selected</span>
</div>
<div class="zoom" role="group" aria-label="Deck zoom">
<button type="button" class="zbtn" id="zoom-out" aria-label="Zoom out">−</button>
<span class="zlabel" id="zoom-label">100%</span>
<button type="button" class="zbtn" id="zoom-in" aria-label="Zoom in">+</button>
</div>
</div>
<div class="deck-scroll">
<div class="fuselage" id="fuselage">
<div class="nose" aria-hidden="true"></div>
<div class="cabin" id="cabin"><!-- rows injected by script.js --></div>
<div class="tail" aria-hidden="true"></div>
</div>
</div>
<div class="col-key" aria-hidden="true">
<span>A</span><span>B</span><span>C</span><span class="aisle"></span><span>D</span><span>E</span><span class="aisle"></span><span>F</span><span>G</span><span>K</span>
</div>
</section>
<aside class="summary" aria-label="Selection summary">
<div class="pax-card">
<span class="k">Selecting for</span>
<div class="pax-row">
<span class="avatar">AR</span>
<div>
<strong>Amara Reyes</strong>
<span class="muted">Adult · Up to 2 seats</span>
</div>
</div>
</div>
<h2 class="sum-title">Your seats</h2>
<ul class="seat-list" id="seat-list">
<li class="empty" id="empty-state">No seats selected yet. Tap any open seat to choose.</li>
</ul>
<dl class="totals">
<div class="t-row"><dt>Seats</dt><dd id="count-out">0</dd></div>
<div class="t-row"><dt>Subtotal</dt><dd id="subtotal-out">$0.00</dd></div>
<div class="t-row total"><dt>Total</dt><dd id="total-out">$0.00</dd></div>
</dl>
<button type="button" class="cta" id="confirm-btn" disabled>Confirm selection</button>
<p class="fineprint">Prices include taxes. Seats held for 20 minutes.</p>
</aside>
</div>
</main>
<div class="tooltip" id="tooltip" role="status" aria-live="polite" hidden></div>
<div class="toast-wrap" id="toast-wrap" aria-live="assertive"></div>
<script src="script.js"></script>
</body>
</html>Seat Map Selector
A status-forward seat-map selector for Skyhaven Air flight SH 218 (JFK → LHR) aboard an A350-900. The fuselage renders three cabins — a 1-2-1 First suite, a 1-2-1 lie-flat Business zone, and a 7-across Economy section with marked exit and bulkhead rows. Each seat is a real button with a slot-style cushion, aisle gaps, and an EXIT badge where the over-wing doors sit. A boarding-pass header shows the route, flight number, aircraft, gate, and a Boarding status pill.
Selection is fully interactive. Tapping an open seat toggles it on or off, animates it into the summary list, and updates the live subtotal and total with tabular figures. A per-passenger guard caps the selection at two seats and raises a toast when you exceed it. Hovering or keyboard-focusing any seat surfaces a tooltip with the seat number, type, and price, while First and Business seats read as included. The legend maps every state — available, extra legroom, exit row, occupied, and selected.
Occupancy and pricing are generated deterministically so the map looks lived-in without a backend, and a deck-zoom control scales the fuselage from 80% to 140% for accurate tapping at ~360px. Everything is vanilla HTML, CSS, and JavaScript with no dependencies, delegated event handling, and accessible labels throughout.
Illustrative UI only — fictional airline, not a real booking or flight system.