Ticketing — Seat Picker Widget
A standalone, zoomable seat-map picker widget for event ticketing. Renders a tiered seating chart with a color-coded legend, per-seat hover tooltips showing row, seat number and price, and tap-to-select with a six-seat order guard. A best-available button auto-picks the highest tier contiguous block, while a live chip bar, running total and countdown keep the selection state clear. Built with semantic markup, keyboard support and vanilla JavaScript only.
MCP
Code
:root {
--brand: #7c3aed;
--brand-d: #6d28d9;
--ink: #0e0e16;
--ink-2: #3a3a4d;
--muted: #6c6c80;
--bg: #f5f4f9;
--surface: #ffffff;
--line: rgba(14, 14, 22, 0.1);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--accent: #ff3d81;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--tier-vip: #ff3d81;
--tier-front: #7c3aed;
--tier-mid: #2563eb;
--tier-rear: #0d9488;
--seat-taken: #c9c9d6;
--seat-sel: #16a34a;
--shadow-sm: 0 2px 8px rgba(14, 14, 22, 0.08);
--shadow-md: 0 12px 32px rgba(14, 14, 22, 0.14);
--shadow-lg: 0 24px 60px rgba(14, 14, 22, 0.2);
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 16px;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(124, 58, 237, 0.16), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(255, 61, 129, 0.14), transparent 60%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
}
.widget {
width: 100%;
max-width: 760px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
/* ---------- header ---------- */
.head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
padding: 22px 24px;
background: linear-gradient(135deg, var(--ink) 0%, #221a3f 55%, var(--brand-d) 130%);
color: #fff;
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255, 61, 129, 0.2);
color: #ffd0e2;
border: 1px solid rgba(255, 61, 129, 0.45);
}
.head h1 {
margin: 10px 0 6px;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.15;
}
.meta {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.78);
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.dot {
opacity: 0.5;
}
.countdown {
flex: none;
text-align: right;
display: flex;
flex-direction: column;
gap: 2px;
}
.cd-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.6);
}
.cd-time {
font-variant-numeric: tabular-nums;
font-weight: 800;
font-size: 20px;
color: #fff;
}
/* ---------- legend ---------- */
.legend {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
padding: 14px 24px;
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.legend-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
}
.legend-list li {
display: inline-flex;
align-items: center;
gap: 7px;
}
.swatch {
width: 14px;
height: 14px;
border-radius: 4px;
display: inline-block;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.swatch[data-tier="vip"] { background: var(--tier-vip); }
.swatch[data-tier="front"] { background: var(--tier-front); }
.swatch[data-tier="mid"] { background: var(--tier-mid); }
.swatch[data-tier="rear"] { background: var(--tier-rear); }
.swatch.state-taken { background: var(--seat-taken); }
.swatch.state-sel { background: var(--seat-sel); }
.legend-state {
color: var(--muted);
}
.zoom-ctrl {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
}
.zoom-ctrl button {
width: 28px;
height: 28px;
border: none;
background: var(--surface);
border-radius: 999px;
font-size: 17px;
font-weight: 700;
color: var(--ink);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: transform 0.12s ease, background 0.12s ease;
}
.zoom-ctrl button:hover { background: var(--brand); color: #fff; }
.zoom-ctrl button:active { transform: scale(0.92); }
.zoom-ctrl button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.zoom-val {
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
min-width: 40px;
text-align: center;
color: var(--ink-2);
}
/* ---------- map ---------- */
.map-wrap {
position: relative;
padding: 22px 24px 18px;
background:
repeating-linear-gradient(0deg, transparent 0 23px, rgba(14, 14, 22, 0.02) 23px 24px);
}
.stage {
margin: 0 auto 22px;
width: min(70%, 420px);
text-align: center;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.4em;
color: #fff;
padding: 10px 0;
border-radius: 0 0 var(--r-lg) var(--r-lg);
background: linear-gradient(180deg, var(--brand) 0%, var(--brand-d) 100%);
box-shadow: 0 10px 24px rgba(124, 58, 237, 0.35);
}
.map-scroll {
overflow: auto;
padding: 4px;
display: flex;
justify-content: center;
}
.seat-grid {
display: inline-flex;
flex-direction: column;
gap: 6px;
transform-origin: top center;
transition: transform 0.18s ease;
}
.seat-row {
display: flex;
align-items: center;
gap: 6px;
}
.row-label {
width: 18px;
flex: none;
text-align: center;
font-size: 10px;
font-weight: 700;
color: var(--muted);
}
.aisle {
width: 14px;
flex: none;
}
.seat {
width: 22px;
height: 22px;
border: none;
border-radius: 6px 6px 4px 4px;
cursor: pointer;
padding: 0;
position: relative;
background: var(--tier-mid);
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.18);
transition: transform 0.1s ease, box-shadow 0.1s ease, filter 0.1s ease;
}
.seat[data-tier="vip"] { background: var(--tier-vip); }
.seat[data-tier="front"] { background: var(--tier-front); }
.seat[data-tier="mid"] { background: var(--tier-mid); }
.seat[data-tier="rear"] { background: var(--tier-rear); }
.seat:hover:not(.taken) {
transform: translateY(-2px) scale(1.12);
z-index: 2;
}
.seat:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 2px;
z-index: 3;
}
.seat.taken {
background: var(--seat-taken);
cursor: not-allowed;
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.1);
}
.seat.selected {
background: var(--seat-sel) !important;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--seat-sel), inset 0 -2px 0 rgba(0, 0, 0, 0.2);
transform: translateY(-1px) scale(1.06);
z-index: 2;
}
.seat.selected::after {
content: "✓";
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 12px;
font-weight: 800;
color: #fff;
}
.tooltip {
position: absolute;
pointer-events: none;
z-index: 10;
background: var(--ink);
color: #fff;
font-size: 12px;
font-weight: 600;
padding: 7px 10px;
border-radius: var(--r-sm);
box-shadow: var(--shadow-md);
opacity: 0;
transform: translate(-50%, -6px);
transition: opacity 0.1s ease;
white-space: nowrap;
}
.tooltip.show { opacity: 1; }
.tooltip strong { color: var(--accent); }
/* ---------- summary ---------- */
.summary {
padding: 16px 24px 6px;
border-top: 1px dashed var(--line);
position: relative;
}
.summary::before,
.summary::after {
content: "";
position: absolute;
top: -10px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line);
}
.summary::before { left: -10px; }
.summary::after { right: -10px; }
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 34px;
align-items: center;
}
.chips-empty {
font-size: 13px;
color: var(--muted);
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 6px 5px 12px;
font-size: 12.5px;
font-weight: 600;
animation: pop 0.18s ease;
}
.chip .seat-tag {
font-weight: 800;
}
.chip .chip-price {
color: var(--muted);
font-weight: 600;
}
.chip-tier {
width: 9px;
height: 9px;
border-radius: 3px;
flex: none;
}
.chip button {
width: 20px;
height: 20px;
border: none;
border-radius: 50%;
background: rgba(14, 14, 22, 0.08);
color: var(--ink-2);
cursor: pointer;
font-size: 13px;
line-height: 1;
display: grid;
place-items: center;
transition: background 0.12s ease;
}
.chip button:hover { background: var(--danger); color: #fff; }
@keyframes pop {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.summary-foot {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-top: 14px;
flex-wrap: wrap;
}
.totals {
display: flex;
align-items: baseline;
gap: 10px;
}
.totals-count {
font-size: 13px;
font-weight: 600;
color: var(--muted);
}
.totals-price {
font-size: 24px;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.actions {
display: flex;
gap: 8px;
}
.btn {
border: none;
border-radius: var(--r-md);
font-family: inherit;
font-size: 14px;
font-weight: 700;
padding: 11px 16px;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease, opacity 0.12s ease;
}
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn:active { transform: translateY(1px); }
.btn.ghost {
background: var(--bg);
color: var(--ink);
border: 1px solid var(--line);
}
.btn.ghost:hover { background: #ececf4; }
.btn.primary {
background: linear-gradient(135deg, var(--brand) 0%, var(--accent) 130%);
color: #fff;
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.35);
}
.btn.primary:hover:not(:disabled) { box-shadow: 0 12px 28px rgba(124, 58, 237, 0.45); }
.btn.primary:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.maxnote {
margin: 8px 0 18px;
padding: 0 24px;
font-size: 12px;
color: var(--muted);
text-align: right;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 50;
max-width: 90vw;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast.warn { background: var(--warn); }
.toast.ok { background: var(--ok); }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body { padding: 14px 8px; }
.head { flex-direction: column; }
.countdown { text-align: left; }
.head h1 { font-size: 19px; }
.legend { padding: 12px 16px; }
.map-wrap { padding: 18px 12px 14px; }
.summary, .maxnote { padding-left: 16px; padding-right: 16px; }
.summary-foot { flex-direction: column; align-items: stretch; }
.actions { justify-content: space-between; }
.actions .btn { flex: 1; padding: 11px 8px; }
.seat { width: 20px; height: 20px; }
}(function () {
"use strict";
var MAX_SEATS = 6;
var TIERS = {
vip: { label: "VIP Pit", price: 240, color: "#ff3d81" },
front: { label: "Front Block", price: 165, color: "#7c3aed" },
mid: { label: "Mid Tier", price: 98, color: "#2563eb" },
rear: { label: "Rear Stand", price: 55, color: "#0d9488" }
};
// Row layout: letter, tier, seat count. Aisle splits each row in two blocks.
var ROWS = [
{ row: "A", tier: "vip" },
{ row: "B", tier: "vip" },
{ row: "C", tier: "front" },
{ row: "D", tier: "front" },
{ row: "E", tier: "front" },
{ row: "F", tier: "mid" },
{ row: "G", tier: "mid" },
{ row: "H", tier: "mid" },
{ row: "J", tier: "mid" },
{ row: "K", tier: "rear" },
{ row: "L", tier: "rear" }
];
var SEATS_PER_BLOCK = 8; // two blocks per row
var grid = document.getElementById("seat-grid");
var tooltip = document.getElementById("tooltip");
var mapWrap = tooltip.parentElement;
var chips = document.getElementById("chips");
var chipsEmpty = document.getElementById("chips-empty");
var selCount = document.getElementById("sel-count");
var selTotal = document.getElementById("sel-total");
var checkoutBtn = document.getElementById("checkout");
var toastEl = document.getElementById("toast");
var selected = []; // array of seat ids in selection order
var seatMap = {}; // id -> { el, row, num, tier, taken }
// ---- deterministic pseudo-random for stable "taken" seats ----
function seeded(n) {
var x = Math.sin(n * 12.9898) * 43758.5453;
return x - Math.floor(x);
}
// ---- build grid ----
var seedCounter = 1;
ROWS.forEach(function (def) {
var rowEl = document.createElement("div");
rowEl.className = "seat-row";
rowEl.setAttribute("role", "row");
var lblL = document.createElement("span");
lblL.className = "row-label";
lblL.textContent = def.row;
rowEl.appendChild(lblL);
for (var block = 0; block < 2; block++) {
for (var s = 1; s <= SEATS_PER_BLOCK; s++) {
var num = block * SEATS_PER_BLOCK + s;
var id = def.row + num;
var taken = seeded(seedCounter++) < 0.22;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "seat" + (taken ? " taken" : "");
btn.dataset.tier = def.tier;
btn.dataset.id = id;
btn.dataset.row = def.row;
btn.dataset.num = num;
btn.setAttribute("role", "gridcell");
btn.setAttribute(
"aria-label",
"Row " + def.row + " seat " + num + ", " + TIERS[def.tier].label +
", $" + TIERS[def.tier].price + (taken ? ", unavailable" : "")
);
if (taken) btn.setAttribute("aria-disabled", "true");
rowEl.appendChild(btn);
seatMap[id] = { el: btn, row: def.row, num: num, tier: def.tier, taken: taken };
}
if (block === 0) {
var aisle = document.createElement("span");
aisle.className = "aisle";
aisle.setAttribute("aria-hidden", "true");
rowEl.appendChild(aisle);
}
}
var lblR = document.createElement("span");
lblR.className = "row-label";
lblR.textContent = def.row;
rowEl.appendChild(lblR);
grid.appendChild(rowEl);
});
// ---- toast helper ----
var toastTimer;
function toast(msg, kind) {
toastEl.textContent = msg;
toastEl.className = "toast show" + (kind ? " " + kind : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.className = "toast" + (kind ? " " + kind : "");
}, 2200);
}
// ---- selection ----
function toggleSeat(id) {
var seat = seatMap[id];
if (!seat || seat.taken) return;
var idx = selected.indexOf(id);
if (idx > -1) {
selected.splice(idx, 1);
seat.el.classList.remove("selected");
seat.el.setAttribute("aria-pressed", "false");
} else {
if (selected.length >= MAX_SEATS) {
toast("Limit of " + MAX_SEATS + " seats per order reached.", "warn");
return;
}
selected.push(id);
seat.el.classList.add("selected");
seat.el.setAttribute("aria-pressed", "true");
}
render();
}
function removeSeat(id) {
var seat = seatMap[id];
if (!seat) return;
var idx = selected.indexOf(id);
if (idx > -1) selected.splice(idx, 1);
seat.el.classList.remove("selected");
seat.el.setAttribute("aria-pressed", "false");
render();
}
function render() {
// chips
chips.querySelectorAll(".chip").forEach(function (c) { c.remove(); });
chipsEmpty.style.display = selected.length ? "none" : "";
var total = 0;
selected.forEach(function (id) {
var seat = seatMap[id];
var t = TIERS[seat.tier];
total += t.price;
var chip = document.createElement("span");
chip.className = "chip";
var dot = document.createElement("span");
dot.className = "chip-tier";
dot.style.background = t.color;
var tag = document.createElement("span");
tag.className = "seat-tag";
tag.textContent = seat.row + seat.num;
var price = document.createElement("span");
price.className = "chip-price";
price.textContent = "$" + t.price;
var rm = document.createElement("button");
rm.type = "button";
rm.setAttribute("aria-label", "Remove seat " + seat.row + seat.num);
rm.textContent = "×";
rm.addEventListener("click", function () { removeSeat(id); });
chip.appendChild(dot);
chip.appendChild(tag);
chip.appendChild(price);
chip.appendChild(rm);
chips.appendChild(chip);
});
selCount.textContent = selected.length + (selected.length === 1 ? " seat" : " seats");
selTotal.textContent = "$" + total.toLocaleString("en-US");
checkoutBtn.disabled = selected.length === 0;
}
// ---- best available: pick MAX best contiguous seats by tier priority ----
function bestAvailable() {
// clear current selection first
selected.slice().forEach(function (id) { removeSeat(id); });
var order = ["vip", "front", "mid", "rear"];
var want = MAX_SEATS;
for (var t = 0; t < order.length && want > 0; t++) {
var rowsInTier = ROWS.filter(function (r) { return r.tier === order[t]; });
for (var ri = 0; ri < rowsInTier.length && want > 0; ri++) {
var picked = pickContiguous(rowsInTier[ri].row, want);
if (picked.length) {
picked.forEach(function (id) {
selected.push(id);
var s = seatMap[id];
s.el.classList.add("selected");
s.el.setAttribute("aria-pressed", "true");
});
want -= picked.length;
}
}
}
render();
if (selected.length) {
var first = seatMap[selected[0]];
first.el.focus();
toast("Picked " + selected.length + " best seats in " + TIERS[first.tier].label + ".", "ok");
} else {
toast("No seats available right now.", "warn");
}
}
// find a contiguous free run of up to `want` seats within a row block
function pickContiguous(row, want) {
var best = [];
for (var block = 0; block < 2; block++) {
var run = [];
for (var s = 1; s <= SEATS_PER_BLOCK; s++) {
var num = block * SEATS_PER_BLOCK + s;
var id = row + num;
var seat = seatMap[id];
if (seat && !seat.taken) {
run.push(id);
if (run.length > best.length) best = run.slice();
if (best.length >= want) return best.slice(0, want);
} else {
run = [];
}
}
}
return best.slice(0, want);
}
// ---- tooltip ----
function showTooltip(seatEl) {
var seat = seatMap[seatEl.dataset.id];
if (!seat) return;
var t = TIERS[seat.tier];
var status = seat.taken ? " · <strong>Taken</strong>" : "";
tooltip.innerHTML =
"Row " + seat.row + " · Seat " + seat.num + " — <strong>$" + t.price + "</strong>" +
"<br>" + t.label + status;
var wrapRect = mapWrap.getBoundingClientRect();
var sRect = seatEl.getBoundingClientRect();
var x = sRect.left - wrapRect.left + sRect.width / 2;
var y = sRect.top - wrapRect.top;
tooltip.style.left = x + "px";
tooltip.style.top = (y - 8) + "px";
tooltip.classList.add("show");
tooltip.setAttribute("aria-hidden", "false");
}
function hideTooltip() {
tooltip.classList.remove("show");
tooltip.setAttribute("aria-hidden", "true");
}
// ---- event delegation ----
grid.addEventListener("click", function (e) {
var seatEl = e.target.closest(".seat");
if (!seatEl) return;
if (seatEl.classList.contains("taken")) {
toast("Row " + seatEl.dataset.row + seatEl.dataset.num + " is already taken.", "warn");
return;
}
toggleSeat(seatEl.dataset.id);
});
grid.addEventListener("mouseover", function (e) {
var seatEl = e.target.closest(".seat");
if (seatEl) showTooltip(seatEl);
});
grid.addEventListener("mouseout", function (e) {
if (e.target.closest(".seat")) hideTooltip();
});
grid.addEventListener("focusin", function (e) {
var seatEl = e.target.closest(".seat");
if (seatEl) showTooltip(seatEl);
});
grid.addEventListener("focusout", hideTooltip);
// ---- zoom ----
var zoom = 1;
var ZMIN = 0.7, ZMAX = 1.6, ZSTEP = 0.15;
var zoomVal = document.getElementById("zoom-val");
function applyZoom() {
grid.style.transform = "scale(" + zoom + ")";
zoomVal.textContent = Math.round(zoom * 100) + "%";
}
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();
});
// ---- controls ----
document.getElementById("best-avail").addEventListener("click", bestAvailable);
document.getElementById("clear-sel").addEventListener("click", function () {
if (!selected.length) { toast("Nothing to clear."); return; }
selected.slice().forEach(function (id) { removeSeat(id); });
toast("Selection cleared.");
});
checkoutBtn.addEventListener("click", function () {
if (!selected.length) return;
var total = selected.reduce(function (sum, id) { return sum + TIERS[seatMap[id].tier].price; }, 0);
toast("Holding " + selected.length + " seats · $" + total.toLocaleString("en-US") + " — demo only.", "ok");
});
// ---- countdown ----
var deadline = Date.now() + 3 * 3600 * 1000 + 47 * 60 * 1000 + 12 * 1000;
var cdEl = document.getElementById("countdown");
function tick() {
var diff = Math.max(0, deadline - Date.now());
var h = Math.floor(diff / 3600000);
var m = Math.floor((diff % 3600000) / 60000);
var s = Math.floor((diff % 60000) / 1000);
function pad(n) { return n < 10 ? "0" + n : "" + n; }
cdEl.textContent = pad(h) + ":" + pad(m) + ":" + pad(s);
}
tick();
setInterval(tick, 1000);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Seat Picker — Solaris Live</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="widget" aria-label="Seat picker">
<header class="head">
<div class="head-info">
<span class="pill">Live On Sale</span>
<h1>Aurora Skyline Tour — Night One</h1>
<p class="meta">
<span>Helios Arena, Marbridge</span>
<span class="dot" aria-hidden="true">•</span>
<span>Sat 27 Jun 2026 · 8:00 PM</span>
</p>
</div>
<div class="countdown" aria-live="polite">
<span class="cd-label">Sale closes in</span>
<span class="cd-time" id="countdown">--:--:--</span>
</div>
</header>
<section class="legend" aria-label="Seat tiers">
<ul class="legend-list">
<li><span class="swatch" data-tier="vip"></span> VIP Pit — $240</li>
<li><span class="swatch" data-tier="front"></span> Front Block — $165</li>
<li><span class="swatch" data-tier="mid"></span> Mid Tier — $98</li>
<li><span class="swatch" data-tier="rear"></span> Rear Stand — $55</li>
<li class="legend-state"><span class="swatch state-taken"></span> Taken</li>
<li class="legend-state"><span class="swatch state-sel"></span> Selected</li>
</ul>
<div class="zoom-ctrl" role="group" aria-label="Zoom controls">
<button type="button" id="zoom-out" aria-label="Zoom out">−</button>
<span id="zoom-val" class="zoom-val">100%</span>
<button type="button" id="zoom-in" aria-label="Zoom in">+</button>
</div>
</section>
<div class="map-wrap">
<div class="stage" aria-hidden="true">STAGE</div>
<div class="map-scroll">
<div class="seat-grid" id="seat-grid" role="grid" aria-label="Seat map"></div>
</div>
<div class="tooltip" id="tooltip" role="status" aria-hidden="true"></div>
</div>
<section class="summary" aria-label="Selection summary">
<div class="chips" id="chips">
<span class="chips-empty" id="chips-empty">No seats selected — tap a seat or use Best available.</span>
</div>
<div class="summary-foot">
<div class="totals">
<span class="totals-count" id="sel-count">0 seats</span>
<span class="totals-price" id="sel-total">$0</span>
</div>
<div class="actions">
<button type="button" class="btn ghost" id="best-avail">Best available</button>
<button type="button" class="btn ghost" id="clear-sel">Clear</button>
<button type="button" class="btn primary" id="checkout" disabled>Continue</button>
</div>
</div>
</section>
<p class="maxnote">Up to <strong>6 seats</strong> per order.</p>
</main>
<div class="toast" id="toast" role="alert" aria-live="assertive"></div>
<script src="script.js"></script>
</body>
</html>Seat Picker Widget
A self-contained seat-selection widget modeled on a real arena flow. The map renders eleven rows split into two blocks by a center aisle, each row color-coded by tier — VIP Pit, Front Block, Mid Tier and Rear Stand — with a legend that doubles as a price key. Some seats are pre-marked as taken so the chart reads like a live on-sale event. A stage banner anchors the orientation, and zoom controls scale the grid from 70% to 160% for crowded sections.
Hovering or focusing a seat raises a tooltip with the row, seat number, tier and price. Selecting a seat toggles a green check and pushes a removable chip into the summary bar; a guard caps the order at six seats and surfaces a toast when the limit is hit. The running total updates instantly, and the Continue button enables only once at least one seat is held. Best available clears the current pick and auto-selects the highest tier contiguous run it can find, then focuses the first seat.
Everything is keyboard-usable: seats are real buttons with descriptive aria-labels and pressed state, the countdown announces politely, and toasts use an assertive live region. No frameworks, no build step — just semantic HTML, CSS variables and one vanilla script.
Illustrative UI only — fictional events, not a real ticketing service.