Airline — Flight Status Board
A status-forward airline flight tracker built with vanilla JS. Search any flight number to reveal a large status card showing the route with airport codes, 24-hour departure and arrival times, gate, terminal, seat range and an animated plane that travels the route line as the flight progresses. Big colour-coded pills cover On time, Boarding, Delayed, Departed and Cancelled states, while a compact departures list and an auto-refresh pulse keep the board feeling alive and current.
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.05), 0 8px 24px rgba(19, 35, 59, 0.07);
--shadow-lg: 0 12px 40px rgba(19, 35, 59, 0.12);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 460px at 100% -8%, var(--sky-50), transparent 60%),
radial-gradient(900px 420px at -6% 0%, var(--sunrise-50), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
padding: clamp(16px, 4vw, 40px);
}
.tnum { font-variant-numeric: tabular-nums; }
button { font-family: inherit; cursor: pointer; }
.board {
max-width: 720px;
margin: 0 auto;
}
/* ---- header ---- */
.board__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 12px;
color: #fff;
background: linear-gradient(140deg, var(--sky), var(--sky-d));
box-shadow: 0 6px 16px rgba(10, 102, 194, 0.32);
}
.brand__txt { display: flex; flex-direction: column; line-height: 1.15; }
.brand__txt strong { font-size: 17px; font-weight: 800; letter-spacing: -0.02em; }
.brand__txt span { font-size: 12px; color: var(--muted); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.board__live {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
box-shadow: var(--shadow);
}
.board__live-txt .tnum { font-variant-numeric: tabular-nums; }
.pulse {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--ok);
position: relative;
box-shadow: 0 0 0 0 rgba(31, 157, 98, 0.5);
animation: pulse 2.1s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(31, 157, 98, 0.5); }
70% { box-shadow: 0 0 0 8px rgba(31, 157, 98, 0); }
100% { box-shadow: 0 0 0 0 rgba(31, 157, 98, 0); }
}
/* ---- search ---- */
.search {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.search__field {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
padding: 0 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow);
transition: border-color 0.15s, box-shadow 0.15s;
}
.search__field:focus-within {
border-color: var(--sky);
box-shadow: 0 0 0 3px var(--sky-50);
}
.search__icon { color: var(--muted); display: grid; place-items: center; }
.search__field input {
flex: 1;
border: 0;
outline: 0;
background: transparent;
font: inherit;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink);
padding: 14px 0;
}
.search__field input::placeholder { color: var(--muted); text-transform: none; letter-spacing: normal; font-weight: 500; }
.search__btn {
border: 0;
border-radius: var(--r-md);
padding: 0 22px;
font-weight: 700;
font-size: 14.5px;
color: #fff;
background: linear-gradient(140deg, var(--sky), var(--sky-d));
box-shadow: 0 6px 16px rgba(10, 102, 194, 0.3);
transition: transform 0.12s, box-shadow 0.15s, filter 0.15s;
}
.search__btn:hover { filter: brightness(1.06); box-shadow: 0 8px 22px rgba(10, 102, 194, 0.38); }
.search__btn:active { transform: translateY(1px); }
.search__hint {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
margin: 0 0 22px;
font-size: 12.5px;
color: var(--muted);
font-weight: 500;
}
.chip {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
border-radius: 999px;
padding: 5px 12px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
font-variant-numeric: tabular-nums;
transition: all 0.14s;
}
.chip:hover { border-color: var(--sky); color: var(--sky); background: var(--sky-50); }
/* ---- status card ---- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
margin-bottom: 26px;
animation: rise 0.4s cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
padding: 18px 20px 14px;
border-bottom: 1px dashed var(--line-2);
}
.card__flight { display: flex; flex-direction: column; gap: 3px; }
.card__flight b { font-size: 20px; font-weight: 800; letter-spacing: 0.02em; font-variant-numeric: tabular-nums; }
.card__flight span { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.01em;
white-space: nowrap;
}
.pill::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.pill--ontime { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.pill--boarding { color: #fff; background: var(--boarding); animation: blink 1.4s ease-in-out infinite; }
.pill--boarding::before { background: #fff; }
.pill--delayed { color: var(--warn); background: rgba(224, 150, 42, 0.14); }
.pill--departed { color: var(--sky); background: var(--sky-50); }
.pill--cancelled { color: var(--danger); background: rgba(212, 73, 62, 0.13); }
@keyframes blink { 50% { opacity: 0.78; } }
/* route */
.route {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 14px;
padding: 22px 20px 8px;
}
.airport { text-align: center; }
.airport--from { text-align: left; }
.airport--to { text-align: right; }
.airport__code { font-size: 30px; font-weight: 800; letter-spacing: -0.01em; line-height: 1; }
.airport__city { font-size: 12.5px; color: var(--muted); font-weight: 600; margin-top: 4px; }
.airport__time { font-size: 15px; font-weight: 700; margin-top: 8px; font-variant-numeric: tabular-nums; }
.airport__time small { display: block; font-size: 11px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
.airport__time.is-late { color: var(--warn); }
.path { position: relative; width: clamp(90px, 28vw, 200px); padding: 0 6px; }
.path__line {
position: relative;
height: 3px;
border-radius: 3px;
background: var(--line-2);
margin: 0 4px;
}
.path__fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: 3px;
background: linear-gradient(90deg, var(--sky), var(--sunrise));
transition: width 1.1s cubic-bezier(0.3, 0.7, 0.2, 1);
}
.path__plane {
position: absolute;
top: 50%;
left: 0%;
transform: translate(-50%, -50%) rotate(90deg);
color: var(--sky);
transition: left 1.1s cubic-bezier(0.3, 0.7, 0.2, 1);
filter: drop-shadow(0 2px 4px rgba(10, 102, 194, 0.35));
}
.path__dot {
position: absolute;
top: 50%;
width: 9px; height: 9px;
border-radius: 50%;
border: 2px solid var(--surface);
transform: translate(-50%, -50%);
}
.path__dot--a { left: 0; background: var(--sky); }
.path__dot--b { left: 100%; background: var(--sunrise); }
.path__meta { text-align: center; font-size: 11px; color: var(--muted); font-weight: 600; margin-top: 9px; letter-spacing: 0.03em; }
/* details strip */
.details {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
margin: 14px 0 0;
border-top: 1px solid var(--line);
}
.detail {
background: var(--surface);
padding: 14px 12px;
text-align: center;
}
.detail__label { font-size: 10.5px; color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.detail__value { font-size: 17px; font-weight: 800; margin-top: 4px; font-variant-numeric: tabular-nums; }
.detail__value.is-accent { color: var(--sunrise); }
.card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 20px;
background: var(--cloud);
font-size: 12.5px;
color: var(--muted);
font-weight: 600;
}
.card__refresh {
display: inline-flex;
align-items: center;
gap: 7px;
}
.card__refresh .pulse { width: 7px; height: 7px; }
.card--empty {
padding: 44px 20px;
text-align: center;
color: var(--muted);
}
.card--empty b { display: block; color: var(--ink); font-size: 16px; margin-bottom: 6px; }
/* ---- departures ---- */
.departures__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.departures__head h2 { margin: 0; font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
.departures__sub { font-size: 12px; color: var(--muted); font-weight: 600; }
.dep-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.dep {
display: grid;
grid-template-columns: 64px 1fr auto auto;
align-items: center;
gap: 12px;
width: 100%;
text-align: left;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
box-shadow: var(--shadow);
transition: transform 0.13s, border-color 0.13s, box-shadow 0.13s;
}
.dep:hover { transform: translateY(-1px); border-color: var(--sky); box-shadow: 0 8px 22px rgba(10, 102, 194, 0.14); }
.dep:active { transform: translateY(0); }
.dep:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.dep.is-active { border-color: var(--sky); background: var(--sky-50); }
.dep__time { font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }
.dep__time small { display: block; font-size: 10.5px; color: var(--muted); font-weight: 700; }
.dep__route { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.dep__route b { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
.dep__route span { font-size: 12px; color: var(--muted); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dep__gate { font-size: 12px; color: var(--ink-2); font-weight: 700; text-align: center; }
.dep__gate small { display: block; font-size: 10px; color: var(--muted); font-weight: 700; text-transform: uppercase; }
.tag {
font-size: 11px;
font-weight: 700;
padding: 5px 10px;
border-radius: 999px;
white-space: nowrap;
}
.tag--ontime { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.tag--boarding { color: #fff; background: var(--boarding); }
.tag--delayed { color: var(--warn); background: rgba(224, 150, 42, 0.14); }
.tag--departed { color: var(--sky); background: var(--sky-50); }
.tag--cancelled { color: var(--danger); background: rgba(212, 73, 62, 0.13); }
/* ---- 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: 12px 18px;
border-radius: 999px;
box-shadow: 0 12px 34px rgba(19, 35, 59, 0.34);
opacity: 0;
pointer-events: none;
transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.2, 1), opacity 0.28s;
z-index: 50;
max-width: calc(100% - 32px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---- responsive ---- */
@media (max-width: 520px) {
body { padding: 14px; }
.board__live-txt { display: none; }
.board__live { padding: 8px; }
.search { flex-wrap: wrap; }
.search__field { flex: 1 1 100%; }
.search__btn { flex: 1; padding: 13px; }
.airport__code { font-size: 25px; }
.details { grid-template-columns: repeat(2, 1fr); }
.dep { grid-template-columns: 54px 1fr auto; gap: 10px; }
.dep__gate { display: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}(function () {
"use strict";
// ---- fictional flight data ----
var FLIGHTS = {
AX482: {
no: "AX482", airline: "Aerolux",
from: { code: "JFK", city: "New York", time: "08:40", actual: "08:40" },
to: { code: "LHR", city: "London", time: "20:15", actual: "20:15" },
status: "boarding", gate: "B12", terminal: "2", seatRows: "1–42",
progress: 0, durationMin: 415, aircraft: "A350-900"
},
AX119: {
no: "AX119", airline: "Aerolux",
from: { code: "SFO", city: "San Francisco", time: "11:20", actual: "11:20" },
to: { code: "NRT", city: "Tokyo", time: "15:05", actual: "15:05" },
status: "departed", gate: "A4", terminal: "2", seatRows: "1–58",
progress: 0.62, durationMin: 645, aircraft: "B787-9"
},
AX730: {
no: "AX730", airline: "Aerolux",
from: { code: "CDG", city: "Paris", time: "14:10", actual: "14:55" },
to: { code: "DXB", city: "Dubai", time: "23:30", actual: "00:15" },
status: "delayed", gate: "D21", terminal: "2", seatRows: "1–48",
progress: 0, durationMin: 380, aircraft: "A330-300"
},
AX256: {
no: "AX256", airline: "Aerolux",
from: { code: "AMS", city: "Amsterdam", time: "09:05", actual: "09:05" },
to: { code: "BCN", city: "Barcelona", time: "11:25", actual: "11:25" },
status: "ontime", gate: "C7", terminal: "2", seatRows: "1–30",
progress: 0, durationMin: 140, aircraft: "A320neo"
},
AX904: {
no: "AX904", airline: "Aerolux",
from: { code: "MAD", city: "Madrid", time: "07:45", actual: "—" },
to: { code: "GRU", city: "São Paulo", time: "16:50", actual: "—" },
status: "cancelled", gate: "—", terminal: "2", seatRows: "—",
progress: 0, durationMin: 615, aircraft: "A350-900"
}
};
var STATUS = {
ontime: { label: "On time", cls: "ontime" },
boarding: { label: "Boarding", cls: "boarding" },
delayed: { label: "Delayed", cls: "delayed" },
departed: { label: "Departed", cls: "departed" },
cancelled:{ label: "Cancelled", cls: "cancelled" }
};
// departures board order
var DEP_ORDER = ["AX256", "AX482", "AX730", "AX119", "AX904"];
var card = document.getElementById("statusCard");
var depList = document.getElementById("depList");
var form = document.getElementById("searchForm");
var input = document.getElementById("flightInput");
var clockEl = document.getElementById("clock");
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("is-show"); }, 2600);
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function fmtDur(min) {
var h = Math.floor(min / 60), m = min % 60;
return h + "h " + (m < 10 ? "0" + m : m) + "m";
}
// ---- render the big status card ----
function renderCard(f) {
var st = STATUS[f.status];
var fromLate = f.from.actual !== f.from.time && f.from.actual !== "—";
var toLate = f.to.actual !== f.to.time && f.to.actual !== "—";
var progPct = f.status === "departed" ? Math.round(f.progress * 100) : 0;
var routeMeta;
if (f.status === "cancelled") routeMeta = "Flight cancelled";
else if (f.status === "departed") routeMeta = "En route · " + progPct + "% complete";
else if (f.status === "boarding") routeMeta = "Now boarding · " + fmtDur(f.durationMin);
else if (f.status === "delayed") routeMeta = "Delayed departure · " + fmtDur(f.durationMin);
else routeMeta = "Scheduled · " + fmtDur(f.durationMin);
card.innerHTML =
'<div class="card__top">' +
'<div class="card__flight">' +
'<b class="tnum">' + esc(f.no) + '</b>' +
'<span>' + esc(f.airline) + ' · ' + esc(f.aircraft) + '</span>' +
'</div>' +
'<span class="pill pill--' + st.cls + '">' + st.label + '</span>' +
'</div>' +
'<div class="route">' +
'<div class="airport airport--from">' +
'<div class="airport__code">' + esc(f.from.code) + '</div>' +
'<div class="airport__city">' + esc(f.from.city) + '</div>' +
'<div class="airport__time' + (fromLate ? ' is-late' : '') + ' tnum">' +
esc(f.from.actual) + '<small>' + (fromLate ? 'sched ' + esc(f.from.time) : 'departs') + '</small>' +
'</div>' +
'</div>' +
'<div class="path">' +
'<div class="path__line">' +
'<div class="path__fill"></div>' +
'<span class="path__dot path__dot--a"></span>' +
'<span class="path__dot path__dot--b"></span>' +
'<span class="path__plane">' +
'<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5Z"/></svg>' +
'</span>' +
'</div>' +
'<div class="path__meta">' + esc(routeMeta) + '</div>' +
'</div>' +
'<div class="airport airport--to">' +
'<div class="airport__code">' + esc(f.to.code) + '</div>' +
'<div class="airport__city">' + esc(f.to.city) + '</div>' +
'<div class="airport__time' + (toLate ? ' is-late' : '') + ' tnum">' +
esc(f.to.actual) + '<small>' + (toLate ? 'sched ' + esc(f.to.time) : 'arrives') + '</small>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="details">' +
'<div class="detail"><div class="detail__label">Gate</div><div class="detail__value is-accent">' + esc(f.gate) + '</div></div>' +
'<div class="detail"><div class="detail__label">Terminal</div><div class="detail__value">' + esc(f.terminal) + '</div></div>' +
'<div class="detail"><div class="detail__label">Seats</div><div class="detail__value tnum">' + esc(f.seatRows) + '</div></div>' +
'<div class="detail"><div class="detail__label">Duration</div><div class="detail__value tnum">' + fmtDur(f.durationMin) + '</div></div>' +
'</div>' +
'<div class="card__foot">' +
'<span class="card__refresh"><span class="pulse"></span>Auto-refreshing</span>' +
'<span>Updated <span class="tnum" id="cardStamp">just now</span></span>' +
'</div>';
// animate the route progress after paint
var fill = card.querySelector(".path__fill");
var plane = card.querySelector(".path__plane");
var target = f.status === "departed" ? f.progress * 100 : (f.status === "cancelled" ? 0 : 0);
requestAnimationFrame(function () {
requestAnimationFrame(function () {
fill.style.width = target + "%";
plane.style.left = target + "%";
});
});
}
function renderEmpty(code) {
card.innerHTML =
'<div class="card--empty">' +
'<b>No flight found' + (code ? ' for ' + esc(code) : '') + '</b>' +
'Check the flight number and try again. Try AX482, AX119, AX730 or AX256.' +
'</div>';
}
// ---- departures list ----
function renderDepartures() {
var html = "";
DEP_ORDER.forEach(function (code) {
var f = FLIGHTS[code];
var st = STATUS[f.status];
var late = f.from.actual !== f.from.time && f.from.actual !== "—";
html +=
'<li>' +
'<button class="dep" type="button" data-flight="' + esc(f.no) + '" aria-label="Track ' + esc(f.no) + ' to ' + esc(f.to.city) + '">' +
'<span class="dep__time tnum">' + esc(f.from.actual !== "—" ? f.from.actual : f.from.time) +
(late ? '<small>was ' + esc(f.from.time) + '</small>' : '<small>' + esc(f.no) + '</small>') +
'</span>' +
'<span class="dep__route">' +
'<b class="tnum">' + esc(f.from.code) + ' → ' + esc(f.to.code) + '</b>' +
'<span>' + esc(f.from.city) + ' to ' + esc(f.to.city) + '</span>' +
'</span>' +
'<span class="dep__gate">' + esc(f.gate) + '<small>gate</small></span>' +
'<span class="tag tag--' + st.cls + '">' + st.label + '</span>' +
'</button>' +
'</li>';
});
depList.innerHTML = html;
}
function setActiveRow(code) {
var rows = depList.querySelectorAll(".dep");
rows.forEach(function (r) {
r.classList.toggle("is-active", r.getAttribute("data-flight") === code);
});
}
// ---- track a flight ----
function track(raw, announce) {
var code = String(raw || "").trim().toUpperCase().replace(/\s+/g, "");
if (!code) { toast("Enter a flight number"); return; }
var f = FLIGHTS[code];
if (!f) {
renderEmpty(code);
setActiveRow(null);
toast("Flight " + code + " not found");
return;
}
input.value = code;
renderCard(f);
setActiveRow(code);
if (announce) toast(STATUS[f.status].label + " · " + f.from.code + " → " + f.to.code);
}
// ---- events ----
form.addEventListener("submit", function (e) {
e.preventDefault();
track(input.value, true);
});
document.addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (chip) { track(chip.getAttribute("data-flight"), true); return; }
var dep = e.target.closest(".dep");
if (dep) {
track(dep.getAttribute("data-flight"), true);
card.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
});
// ---- clock + auto-refresh pulse ----
function pad(n) { return n < 10 ? "0" + n : n; }
function tickClock() {
var d = new Date();
clockEl.textContent = pad(d.getUTCHours()) + ":" + pad(d.getUTCMinutes());
}
tickClock();
setInterval(tickClock, 1000 * 20);
// simulate live progress on departed flights + refresh stamp
var refreshes = 0;
setInterval(function () {
var enroute = FLIGHTS.AX119;
if (enroute.progress < 0.99) {
enroute.progress = Math.min(0.99, enroute.progress + 0.015);
}
var stamp = document.getElementById("cardStamp");
if (stamp) {
refreshes++;
stamp.textContent = refreshes === 0 ? "just now" : refreshes + "s ago";
}
// if currently viewing the en-route flight, nudge the plane
var active = depList.querySelector(".dep.is-active");
if (active && active.getAttribute("data-flight") === "AX119") {
var fill = card.querySelector(".path__fill");
var plane = card.querySelector(".path__plane");
if (fill && plane) {
var p = Math.round(enroute.progress * 100) + "%";
fill.style.width = p;
plane.style.left = p;
}
}
}, 4000);
// ---- init ----
renderDepartures();
track("AX482", false);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aerolux — Flight Status Board</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="board" aria-label="Flight status board">
<header class="board__head">
<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.2a.5.5 0 0 0-.5.8L8 11l-2 2H3l1.5 3L7 17l1-2 5.2 3.7a.5.5 0 0 0 .8-.5Z"/></svg>
</span>
<div class="brand__txt">
<strong>Aerolux</strong>
<span>Flight Status</span>
</div>
</div>
<div class="board__live">
<span class="pulse" aria-hidden="true"></span>
<span class="board__live-txt">Live · <span id="clock">--:--</span> UTC</span>
</div>
</header>
<form class="search" id="searchForm" role="search" autocomplete="off">
<label class="search__field">
<span class="search__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</span>
<input id="flightInput" type="text" inputmode="latin" placeholder="Flight number — e.g. AX482" aria-label="Search flight number" value="AX482" />
</label>
<button class="search__btn" type="submit">Track</button>
</form>
<p class="search__hint" id="searchHint">Try <button type="button" class="chip" data-flight="AX482">AX482</button><button type="button" class="chip" data-flight="AX119">AX119</button><button type="button" class="chip" data-flight="AX730">AX730</button><button type="button" class="chip" data-flight="AX256">AX256</button></p>
<section class="card" id="statusCard" aria-live="polite">
<!-- populated by JS -->
</section>
<section class="departures" aria-label="Departures">
<div class="departures__head">
<h2>Departures · Terminal 2</h2>
<span class="departures__sub">Tap a flight to track</span>
</div>
<ul class="dep-list" id="depList"></ul>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Flight Status Board
A clean, aviation-styled status board for tracking a single flight. Type a flight number (or tap one of the suggested chips) and the big status card updates with the full route — origin and destination airport codes, cities, 24-hour times, gate, terminal, seat range and duration. A large colour-coded pill states the live status: On time, Boarding, Delayed, Departed or Cancelled, with the boarding pill gently blinking to draw the eye.
The centrepiece is the animated route line: a plane icon glides from the origin dot toward the destination dot, with a gradient fill tracking how far along the journey the flight has travelled. Delayed flights surface both scheduled and revised times in an amber accent, and a live UTC clock plus an auto-refresh pulse in the card footer make the widget feel connected to a real operations feed.
Below the card sits a compact Terminal 2 departures list. Each row shows the time, route, gate and a status tag; tapping a row tracks that flight and highlights it. Everything is vanilla JS with tabular figures for times and flight numbers, a small toast() helper for feedback, full keyboard support and a responsive layout that collapses gracefully down to narrow mobile passenger screens.
Illustrative UI only — fictional airline, not a real booking or flight system.