Airline — Results List
A polished flight-results screen for a fictional airline search. Browse fare cards showing carrier, depart-to-arrive times, route duration, stop count and per-person price, then narrow them with a sticky filter rail for stops, departure window, max price and airlines. Sort tabs flip the list between best value, cheapest and fastest, each card expands to compare Economy Light, Flex and Business fares, and selecting a fare fires a confirmation toast.
MCP
Code
:root {
--sky: #0a66c2;
--sky-d: #084e95;
--sky-50: #e9f2fb;
--cloud: #f5f8fc;
--sunrise: #ff7a33;
--sunrise-50: #fff0e7;
--ink: #13233b;
--ink-2: #3a4d68;
--muted: #6b7c93;
--bg: #f5f8fc;
--surface: #ffffff;
--line: rgba(19, 35, 59, 0.1);
--line-2: rgba(19, 35, 59, 0.18);
--ok: #1f9d62;
--warn: #e0962a;
--danger: #d4493e;
--boarding: #1f9d62;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.06), 0 1px 3px rgba(19, 35, 59, 0.05);
--shadow-md: 0 4px 16px rgba(19, 35, 59, 0.08);
--shadow-lg: 0 12px 34px rgba(19, 35, 59, 0.13);
}
* { 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;
font-feature-settings: "cv05", "ss01";
}
.tnum { font-variant-numeric: tabular-nums; }
/* ── Topbar ───────────────────────────── */
.topbar {
display: flex;
align-items: center;
gap: 18px;
padding: 14px clamp(16px, 4vw, 40px);
background: linear-gradient(180deg, var(--surface), #fbfdff);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.brand { display: flex; align-items: center; gap: 9px; }
.brand-mark {
display: grid; place-items: center;
width: 36px; height: 36px;
border-radius: 11px;
background: linear-gradient(135deg, var(--sky), var(--sky-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.brand-name { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
.brand-name strong { color: var(--sky); font-weight: 800; }
.route-summary {
display: flex; align-items: center; gap: 8px;
margin-left: 6px;
padding: 6px 14px;
background: var(--sky-50);
border: 1px solid rgba(10, 102, 194, 0.18);
border-radius: 999px;
font-size: 13px;
}
.rs-code { font-weight: 800; font-size: 15px; letter-spacing: 0.04em; color: var(--ink); }
.rs-arrow { color: var(--sky); font-weight: 700; }
.rs-meta { color: var(--ink-2); margin-left: 6px; font-weight: 500; }
.edit-search {
margin-left: auto;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
font: inherit; font-weight: 600; font-size: 13px;
padding: 8px 16px;
border-radius: 999px;
cursor: pointer;
transition: border-color .15s, color .15s, background .15s;
}
.edit-search:hover { border-color: var(--sky); color: var(--sky); background: var(--sky-50); }
/* ── Layout ───────────────────────────── */
.layout {
display: grid;
grid-template-columns: 268px minmax(0, 1fr);
gap: 24px;
max-width: 1080px;
margin: 0 auto;
padding: 24px clamp(16px, 4vw, 40px) 56px;
}
/* ── Filter rail ──────────────────────── */
.rail {
position: sticky;
top: 86px;
align-self: start;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--shadow-sm);
}
.rail-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.rail-head h2 { font-size: 15px; margin: 0; letter-spacing: -0.01em; }
.reset-btn {
border: none; background: none; color: var(--sky);
font: inherit; font-weight: 600; font-size: 12.5px;
cursor: pointer; padding: 4px 6px; border-radius: var(--r-sm);
}
.reset-btn:hover { background: var(--sky-50); }
.reset-btn.solid {
background: var(--sky); color: #fff; padding: 9px 18px; margin-top: 6px;
}
.reset-btn.solid:hover { background: var(--sky-d); }
.filter-group { padding: 16px 0; border-top: 1px solid var(--line); }
.filter-group:first-of-type { border-top: none; padding-top: 8px; }
.filter-group h3 {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em;
color: var(--muted); margin: 0 0 11px; font-weight: 700;
}
.seg { display: flex; gap: 6px; background: var(--cloud); padding: 4px; border-radius: 11px; }
.seg-btn {
flex: 1; border: none; background: none; cursor: pointer;
font: inherit; font-weight: 600; font-size: 12.5px; color: var(--ink-2);
padding: 7px 4px; border-radius: 8px; transition: .15s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-active { background: var(--surface); color: var(--sky); box-shadow: var(--shadow-sm); }
.chips { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.chip { position: relative; cursor: pointer; }
.chip input { position: absolute; opacity: 0; pointer-events: none; }
.chip span {
display: flex; flex-direction: column;
padding: 8px 10px; border: 1px solid var(--line-2); border-radius: var(--r-sm);
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
transition: .15s;
}
.chip span small { font-weight: 500; font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.chip:hover span { border-color: var(--sky); }
.chip input:checked + span { background: var(--sky-50); border-color: var(--sky); color: var(--sky); }
.chip input:focus-visible + span { outline: 2px solid var(--sky); outline-offset: 2px; }
.price-row { display: flex; align-items: center; gap: 12px; }
.price-out { font-weight: 700; font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; min-width: 56px; text-align: right; }
input[type="range"] { -webkit-appearance: none; appearance: none; width: 100%; height: 5px; border-radius: 4px; background: var(--sky-50); outline: none; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--sky); border: 3px solid #fff; box-shadow: var(--shadow-sm); cursor: pointer; }
input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--sky); border: 3px solid #fff; cursor: pointer; }
input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--sunrise); outline-offset: 2px; }
.airline-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.airline-row { display: flex; align-items: center; gap: 10px; padding: 6px 4px; cursor: pointer; border-radius: var(--r-sm); }
.airline-row:hover { background: var(--cloud); }
.airline-row input { width: 16px; height: 16px; accent-color: var(--sky); cursor: pointer; }
.airline-dot { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.airline-name { font-size: 13px; font-weight: 600; flex: 1; }
.airline-from { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* ── Sort bar ─────────────────────────── */
.sort-bar {
display: grid; grid-template-columns: repeat(3, 1fr);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.sort-tab {
border: none; background: none; cursor: pointer;
font: inherit; font-weight: 700; font-size: 13.5px; color: var(--ink-2);
padding: 13px 10px;
display: flex; flex-direction: column; gap: 2px; align-items: center;
border-bottom: 3px solid transparent;
transition: .15s;
}
.sort-tab + .sort-tab { border-left: 1px solid var(--line); }
.sort-tab .sub { font-size: 11.5px; font-weight: 500; color: var(--muted); font-variant-numeric: tabular-nums; }
.sort-tab:hover { background: var(--cloud); }
.sort-tab.is-active { color: var(--sky); border-bottom-color: var(--sky); background: var(--sky-50); }
.sort-tab.is-active .sub { color: var(--sky-d); }
.count-line { font-size: 12.5px; color: var(--muted); margin: 14px 2px; }
.count-line span { font-weight: 700; color: var(--ink-2); }
/* ── Flight cards ─────────────────────── */
.flight-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 14px; }
.flight-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: box-shadow .18s, border-color .18s, transform .18s;
}
.flight-card:hover { box-shadow: var(--shadow-md); border-color: var(--line-2); }
.flight-card.is-selected { border-color: var(--sky); box-shadow: 0 0 0 2px var(--sky-50), var(--shadow-md); }
.fc-main { display: grid; grid-template-columns: 1fr auto; gap: 18px; padding: 18px 20px; align-items: center; }
.fc-airline { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.fc-logo {
width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
color: #fff; font-weight: 800; font-size: 12px; flex: none; letter-spacing: -0.02em;
}
.fc-airline-name { font-size: 13px; font-weight: 600; }
.fc-flightno { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.fc-tag {
margin-left: auto; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
padding: 3px 9px; border-radius: 999px;
}
.fc-tag.best { background: var(--sunrise-50); color: #c0531b; }
.fc-tag.fastest { background: var(--sky-50); color: var(--sky); }
.timeline { display: grid; grid-template-columns: auto 1fr auto; gap: 16px; align-items: center; }
.tl-end { text-align: center; }
.tl-time { font-size: 21px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.tl-code { font-size: 12.5px; font-weight: 700; color: var(--ink-2); letter-spacing: 0.03em; }
.tl-end.arr { text-align: right; }
.tl-end.dep { text-align: left; }
.tl-path { display: flex; flex-direction: column; align-items: center; gap: 5px; padding-top: 2px; }
.tl-dur { font-size: 11.5px; color: var(--ink-2); font-weight: 600; font-variant-numeric: tabular-nums; }
.tl-line { position: relative; width: 100%; height: 2px; background: var(--line-2); border-radius: 2px; }
.tl-line::before, .tl-line::after { content: ""; position: absolute; top: 50%; width: 6px; height: 6px; border-radius: 50%; background: var(--sky); transform: translateY(-50%); }
.tl-line::before { left: 0; }
.tl-line::after { right: 0; }
.tl-plane { position: absolute; top: 50%; transform: translate(-50%, -50%); color: var(--sky); background: var(--surface); padding: 0 3px; }
.tl-stops { font-size: 11px; font-weight: 600; }
.tl-stops.direct { color: var(--ok); }
.tl-stops.stop { color: var(--warn); }
.fc-buy { display: flex; flex-direction: column; align-items: flex-end; gap: 7px; text-align: right; border-left: 1px dashed var(--line-2); padding-left: 20px; }
.fc-price { font-size: 23px; font-weight: 800; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.fc-price small { font-size: 11px; font-weight: 500; color: var(--muted); display: block; margin-top: -2px; }
.fc-fares-toggle {
border: 1px solid var(--sky); background: var(--surface); color: var(--sky);
font: inherit; font-weight: 700; font-size: 12.5px;
padding: 8px 16px; border-radius: var(--r-sm); cursor: pointer;
display: inline-flex; align-items: center; gap: 6px;
transition: .15s;
}
.fc-fares-toggle:hover { background: var(--sky-50); }
.fc-fares-toggle .caret { transition: transform .2s; }
.flight-card.is-open .fc-fares-toggle .caret { transform: rotate(180deg); }
/* ── Fare options ─────────────────────── */
.fc-fares { display: none; padding: 0 20px 18px; }
.flight-card.is-open .fc-fares { display: block; }
.fares-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding-top: 16px; border-top: 1px dashed var(--line-2); }
.fare {
border: 1px solid var(--line-2); border-radius: var(--r-md);
padding: 14px; cursor: pointer; transition: .15s; background: var(--surface);
display: flex; flex-direction: column; gap: 9px;
}
.fare:hover { border-color: var(--sky); box-shadow: var(--shadow-sm); }
.fare-name { font-size: 13px; font-weight: 700; }
.fare-price { font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
.fare-price span { font-size: 11px; font-weight: 500; color: var(--muted); }
.fare-perks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.fare-perks li { font-size: 11.5px; color: var(--ink-2); display: flex; align-items: center; gap: 6px; }
.fare-perks li.no { color: var(--muted); }
.fare-perks .ico { flex: none; }
.fare-select {
margin-top: 2px; border: none; background: var(--sky); color: #fff;
font: inherit; font-weight: 700; font-size: 12.5px;
padding: 9px; border-radius: var(--r-sm); cursor: pointer; transition: .15s;
}
.fare-select:hover { background: var(--sky-d); }
.fare.is-eco .fare-select { background: var(--ink-2); }
.fare.is-eco .fare-select:hover { background: var(--ink); }
/* ── Empty / Toast ────────────────────── */
.empty {
text-align: center; padding: 56px 20px;
background: var(--surface); border: 1px dashed var(--line-2); border-radius: var(--r-lg);
}
.empty-title { font-size: 16px; font-weight: 700; margin: 0 0 4px; }
.empty-sub { font-size: 13px; color: var(--muted); margin: 0; }
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 18px);
background: var(--ink); color: #fff;
padding: 12px 20px; border-radius: var(--r-md);
font-size: 13.5px; font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0; pointer-events: none;
transition: opacity .22s, transform .22s;
z-index: 50; max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast .check { color: var(--ok); margin-right: 8px; }
/* ── Responsive ───────────────────────── */
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.rail { position: static; }
}
@media (max-width: 520px) {
.topbar { flex-wrap: wrap; gap: 10px; padding: 12px 16px; }
.route-summary { order: 3; width: 100%; margin-left: 0; }
.edit-search { margin-left: auto; order: 2; }
.rs-meta { margin-left: auto; }
.fc-main { grid-template-columns: 1fr; }
.fc-buy { flex-direction: row; align-items: center; justify-content: space-between; border-left: none; padding-left: 0; border-top: 1px dashed var(--line-2); padding-top: 14px; }
.fares-grid { grid-template-columns: 1fr; }
.tl-time { font-size: 19px; }
.sort-tab { font-size: 12px; padding: 11px 6px; }
.chips { grid-template-columns: 1fr 1fr; }
}(function () {
"use strict";
// ── Fictional flight data ───────────────────────────────
const AIRLINES = {
NV: { name: "Nimbus Air", color: "#0a66c2" },
AU: { name: "Aurora Atlantic", color: "#ff7a33" },
PV: { name: "Polaris Voyage", color: "#1f9d62" },
MR: { name: "Meridian Skyways", color: "#7b5cd6" },
};
// duration in minutes, times 24h, prices USD
const FLIGHTS = [
{
id: "NV482", airline: "NV", flightNo: "NV 482",
depTime: "10:15", depCode: "SFO", arrTime: "06:20", arrCode: "LHR",
dayOffset: "+1", durMin: 545, stops: 0, price: 612, depMins: 615,
fares: { eco: 612, plus: 748, biz: 1990 },
},
{
id: "PV119", airline: "PV", flightNo: "PV 119",
depTime: "07:40", depCode: "SFO", arrTime: "04:05", arrCode: "LHR",
dayOffset: "+1", durMin: 625, stops: 1, stopCode: "YYZ", stopMin: 75, price: 498, depMins: 460,
fares: { eco: 498, plus: 629, biz: 1670 },
},
{
id: "AU707", airline: "AU", flightNo: "AU 707",
depTime: "13:30", depCode: "SFO", arrTime: "08:55", arrCode: "LHR",
dayOffset: "+1", durMin: 625, stops: 0, price: 689, depMins: 810,
fares: { eco: 689, plus: 815, biz: 2140 },
},
{
id: "MR233", airline: "MR", flightNo: "MR 233",
depTime: "21:50", depCode: "SFO", arrTime: "20:15", arrCode: "LHR",
dayOffset: "+1", durMin: 625, stops: 1, stopCode: "ORD", stopMin: 110, price: 544, depMins: 1310,
fares: { eco: 544, plus: 672, biz: 1820 },
},
{
id: "NV906", airline: "NV", flightNo: "NV 906",
depTime: "16:05", depCode: "SFO", arrTime: "10:30", arrCode: "LHR",
dayOffset: "+1", durMin: 625, stops: 0, price: 731, depMins: 965,
fares: { eco: 731, plus: 868, biz: 2280 },
},
];
const FARE_DEFS = [
{ key: "eco", name: "Economy Light", cls: "is-eco", perks: [
{ ok: true, t: "1 personal item" }, { ok: false, t: "No checked bag" }, { ok: false, t: "Seat at check-in" },
]},
{ key: "plus", name: "Economy Flex", cls: "", perks: [
{ ok: true, t: "1 carry-on + 1 bag" }, { ok: true, t: "Free seat select" }, { ok: true, t: "Changes allowed" },
]},
{ key: "biz", name: "Business", cls: "", perks: [
{ ok: true, t: "Lie-flat seat" }, { ok: true, t: "2 checked bags" }, { ok: true, t: "Lounge access" },
]},
];
// ── State ───────────────────────────────────────────────
const state = {
stops: "any",
windows: new Set(),
maxPrice: 1450,
airlines: new Set(Object.keys(AIRLINES)),
sort: "best",
selected: null,
};
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
const fmtMoney = (n) => "$" + n.toLocaleString("en-US");
const fmtDur = (m) => `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
const windowOf = (mins) => {
const h = mins / 60;
if (h < 6) return "early";
if (h < 12) return "morning";
if (h < 18) return "afternoon";
return "evening";
};
// ── Toast ───────────────────────────────────────────────
let toastTimer;
function toast(msg, ok) {
const el = $("#toast");
el.innerHTML = (ok ? '<span class="check">✓</span>' : "") + msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove("show"), 2600);
}
// ── Build airline filter list ───────────────────────────
function buildAirlineList() {
const ul = $("#airlineList");
ul.innerHTML = "";
Object.entries(AIRLINES).forEach(([code, a]) => {
const min = Math.min(...FLIGHTS.filter((f) => f.airline === code).map((f) => f.price));
const li = document.createElement("li");
li.innerHTML = `
<label class="airline-row">
<input type="checkbox" data-airline="${code}" checked />
<span class="airline-dot" style="background:${a.color}"></span>
<span class="airline-name">${a.name}</span>
<span class="airline-from tnum">from ${fmtMoney(min)}</span>
</label>`;
ul.appendChild(li);
});
ul.addEventListener("change", (e) => {
const cb = e.target.closest("input[data-airline]");
if (!cb) return;
const code = cb.dataset.airline;
if (cb.checked) state.airlines.add(code);
else state.airlines.delete(code);
render();
});
}
// ── Filtering + sorting ─────────────────────────────────
function visibleFlights() {
return FLIGHTS.filter((f) => {
if (state.stops === "0" && f.stops !== 0) return false;
if (state.stops === "1" && f.stops > 1) return false;
if (state.windows.size && !state.windows.has(windowOf(f.depMins))) return false;
if (f.price > state.maxPrice) return false;
if (!state.airlines.has(f.airline)) return false;
return true;
});
}
function sortFlights(list) {
const arr = list.slice();
if (state.sort === "cheapest") arr.sort((a, b) => a.price - b.price);
else if (state.sort === "fastest") arr.sort((a, b) => a.durMin - b.durMin);
else arr.sort((a, b) => (a.price + a.durMin * 0.45) - (b.price + b.durMin * 0.45));
return arr;
}
// ── Render a single card ────────────────────────────────
function cardHTML(f, idx) {
const a = AIRLINES[f.airline];
const stopsLabel = f.stops === 0
? '<span class="tl-stops direct">Direct</span>'
: `<span class="tl-stops stop">${f.stops} stop · ${f.stopCode} ${fmtDur(f.stopMin)}</span>`;
let tag = "";
if (state.sort === "best" && idx === 0) tag = '<span class="fc-tag best">Best value</span>';
else if (state.sort === "fastest" && idx === 0) tag = '<span class="fc-tag fastest">Fastest</span>';
const fares = FARE_DEFS.map((d) => `
<div class="fare ${d.cls}" data-fare="${d.key}">
<div class="fare-name">${d.name}</div>
<div class="fare-price tnum">${fmtMoney(f.fares[d.key])} <span>/ person</span></div>
<ul class="fare-perks">
${d.perks.map((p) => `<li class="${p.ok ? "" : "no"}"><span class="ico">${p.ok ? "✓" : "✕"}</span>${p.t}</li>`).join("")}
</ul>
<button type="button" class="fare-select" data-fare-select="${d.key}">Select ${fmtMoney(f.fares[d.key])}</button>
</div>`).join("");
return `
<li>
<article class="flight-card" data-id="${f.id}">
<div class="fc-main">
<div>
<div class="fc-airline">
<span class="fc-logo" style="background:${a.color}">${f.airline}</span>
<div>
<div class="fc-airline-name">${a.name}</div>
<div class="fc-flightno tnum">${f.flightNo} · Boeing 787-9</div>
</div>
${tag}
</div>
<div class="timeline">
<div class="tl-end dep">
<div class="tl-time tnum">${f.depTime}</div>
<div class="tl-code">${f.depCode}</div>
</div>
<div class="tl-path">
<span class="tl-dur tnum">${fmtDur(f.durMin)}</span>
<span class="tl-line"><span class="tl-plane" aria-hidden="true">
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M21 16.5 14 12V5.5a2 2 0 1 0-4 0V12L3 16.5V18l7-2.2V20l-2 1.4V22l4-1 4 1v-.6L14 20v-4.2L21 18v-1.5Z"/></svg>
</span></span>
${stopsLabel}
</div>
<div class="tl-end arr">
<div class="tl-time tnum">${f.arrTime}<sup style="font-size:11px;color:var(--sunrise)">${f.dayOffset}</sup></div>
<div class="tl-code">${f.arrCode}</div>
</div>
</div>
</div>
<div class="fc-buy">
<div class="fc-price tnum">${fmtMoney(f.price)}<small>per person</small></div>
<button type="button" class="fc-fares-toggle" data-toggle aria-expanded="false">
View fares <span class="caret" aria-hidden="true">▾</span>
</button>
</div>
</div>
<div class="fc-fares">
<div class="fares-grid">${fares}</div>
</div>
</article>
</li>`;
}
// ── Render results ──────────────────────────────────────
function render() {
const all = FLIGHTS.length;
const sorted = sortFlights(visibleFlights());
const list = $("#flightList");
const empty = $("#emptyState");
$(".count-line").innerHTML = `<span id="resultCount">${sorted.length}</span> of ${all} flights · prices per person incl. taxes`;
if (!sorted.length) {
list.innerHTML = "";
empty.hidden = false;
return;
}
empty.hidden = true;
list.innerHTML = sorted.map((f, i) => cardHTML(f, i)).join("");
if (state.selected) {
const sel = list.querySelector(`[data-id="${state.selected}"]`);
if (sel) sel.classList.add("is-selected");
}
}
// ── Sort summaries ──────────────────────────────────────
function updateSortSubs() {
const v = FLIGHTS;
$("#cheapSub").textContent = fmtMoney(Math.min(...v.map((f) => f.price)));
$("#fastSub").textContent = fmtDur(Math.min(...v.map((f) => f.durMin)));
const best = sortFlights(v.slice())[0];
$("#bestSub").textContent = `${fmtMoney(best.price)} · ${fmtDur(best.durMin)}`;
}
// ── Event wiring ────────────────────────────────────────
function wire() {
// stops segmented
$$(".seg-btn").forEach((b) => b.addEventListener("click", () => {
$$(".seg-btn").forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
state.stops = b.dataset.stops;
render();
}));
// departure windows
$$('input[name="depwin"]').forEach((cb) => cb.addEventListener("change", () => {
if (cb.checked) state.windows.add(cb.value);
else state.windows.delete(cb.value);
render();
}));
// price range
const range = $("#priceRange");
const out = $("#priceOut");
range.addEventListener("input", () => {
state.maxPrice = +range.value;
out.textContent = fmtMoney(state.maxPrice);
render();
});
// sort tabs
$$(".sort-tab").forEach((t) => t.addEventListener("click", () => {
$$(".sort-tab").forEach((x) => { x.classList.remove("is-active"); x.setAttribute("aria-selected", "false"); });
t.classList.add("is-active");
t.setAttribute("aria-selected", "true");
state.sort = t.dataset.sort;
render();
}));
// delegated clicks within results
$("#flightList").addEventListener("click", (e) => {
const toggle = e.target.closest("[data-toggle]");
if (toggle) {
const card = toggle.closest(".flight-card");
const open = card.classList.toggle("is-open");
toggle.setAttribute("aria-expanded", String(open));
toggle.childNodes[0].nodeValue = open ? "Hide fares " : "View fares ";
return;
}
const fare = e.target.closest("[data-fare-select]");
if (fare) {
const card = fare.closest(".flight-card");
const id = card.dataset.id;
state.selected = id;
$$(".flight-card").forEach((c) => c.classList.remove("is-selected"));
card.classList.add("is-selected");
const f = FLIGHTS.find((x) => x.id === id);
const def = FARE_DEFS.find((d) => d.key === fare.dataset.fareSelect);
toast(`${AIRLINES[f.airline].name} ${f.flightNo} — ${def.name} selected · ${fmtMoney(f.fares[def.key])}`, true);
return;
}
});
// reset
function resetAll() {
state.stops = "any";
state.windows.clear();
state.maxPrice = 1450;
state.airlines = new Set(Object.keys(AIRLINES));
state.selected = null;
$$(".seg-btn").forEach((x, i) => x.classList.toggle("is-active", i === 0));
$$('input[name="depwin"]').forEach((cb) => (cb.checked = false));
$$('input[data-airline]').forEach((cb) => (cb.checked = true));
range.value = 1450;
out.textContent = "$1,450";
render();
toast("Filters reset");
}
$("#resetFilters").addEventListener("click", resetAll);
$("#emptyReset").addEventListener("click", resetAll);
$("#editSearch").addEventListener("click", () => toast("Search editor is illustrative only"));
}
// ── Init ────────────────────────────────────────────────
buildAirlineList();
updateSortSubs();
wire();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyline Air — Flight Results</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>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><path d="M21 16.5 14 12V5.5a2 2 0 1 0-4 0V12L3 16.5V18l7-2.2V20l-2 1.4V22l4-1 4 1v-.6L14 20v-4.2L21 18v-1.5Z" fill="currentColor"/></svg>
</span>
<span class="brand-name">Skyline<strong>Air</strong></span>
</div>
<nav class="route-summary" aria-label="Search route">
<span class="rs-code">SFO</span>
<span class="rs-arrow" aria-hidden="true">→</span>
<span class="rs-code">LHR</span>
<span class="rs-meta">Mon 22 Jun · 1 adult · Economy</span>
</nav>
<button class="edit-search" type="button" id="editSearch">Edit search</button>
</header>
<main class="layout">
<!-- Filter rail -->
<aside class="rail" aria-label="Filters">
<div class="rail-head">
<h2>Filters</h2>
<button type="button" class="reset-btn" id="resetFilters">Reset</button>
</div>
<section class="filter-group">
<h3>Stops</h3>
<div class="seg" role="group" aria-label="Number of stops">
<button type="button" class="seg-btn is-active" data-stops="any">Any</button>
<button type="button" class="seg-btn" data-stops="0">Direct</button>
<button type="button" class="seg-btn" data-stops="1">1 stop</button>
</div>
</section>
<section class="filter-group">
<h3>Departure time</h3>
<div class="chips" role="group" aria-label="Departure window">
<label class="chip"><input type="checkbox" name="depwin" value="early" /><span>Early<small>00–06</small></span></label>
<label class="chip"><input type="checkbox" name="depwin" value="morning" /><span>Morning<small>06–12</small></span></label>
<label class="chip"><input type="checkbox" name="depwin" value="afternoon" /><span>Afternoon<small>12–18</small></span></label>
<label class="chip"><input type="checkbox" name="depwin" value="evening" /><span>Evening<small>18–24</small></span></label>
</div>
</section>
<section class="filter-group">
<h3>Max price</h3>
<div class="price-row">
<input type="range" id="priceRange" min="380" max="1450" step="10" value="1450" aria-label="Maximum price" />
<output id="priceOut" class="price-out">$1,450</output>
</div>
</section>
<section class="filter-group">
<h3>Airlines</h3>
<ul class="airline-list" id="airlineList"></ul>
</section>
</aside>
<!-- Results -->
<section class="results" aria-label="Flight results">
<div class="sort-bar" role="tablist" aria-label="Sort flights">
<button type="button" class="sort-tab is-active" data-sort="best" role="tab" aria-selected="true">
Best <span class="sub" id="bestSub">$612 · 11h 05m</span>
</button>
<button type="button" class="sort-tab" data-sort="cheapest" role="tab" aria-selected="false">
Cheapest <span class="sub" id="cheapSub">$498</span>
</button>
<button type="button" class="sort-tab" data-sort="fastest" role="tab" aria-selected="false">
Fastest <span class="sub" id="fastSub">10h 25m</span>
</button>
</div>
<p class="count-line"><span id="resultCount">5</span> of 5 flights · prices per person incl. taxes</p>
<ul class="flight-list" id="flightList"></ul>
<div class="empty" id="emptyState" hidden>
<p class="empty-title">No flights match your filters</p>
<p class="empty-sub">Try widening your price range or allowing connections.</p>
<button type="button" class="reset-btn solid" id="emptyReset">Reset filters</button>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Results List
A complete flight-search results screen for the fictional SkylineAir, built with vanilla HTML, CSS and JavaScript. The sticky left rail holds the filters — a segmented stops control (Any / Direct / 1 stop), four departure-window chips, a live max-price slider and an airline checklist showing each carrier’s lowest fare. Every control updates the result list instantly, and a reset button restores the defaults. When nothing matches, a friendly empty state offers a one-tap reset.
Each result is a boarding-pass-style card: airline badge and flight number, a departure → arrival timeline with 24h tabular times, route duration, a plane-on-a-line graphic, and the day-offset and stop summary. Sort tabs across the top switch the ordering between Best value, Cheapest and Fastest, each tab annotated with its own headline figure. The top card earns a Best value or Fastest pill depending on the active sort.
Expanding a card reveals three fare classes — Economy Light, Economy Flex and Business — laid out as comparison columns with perk checklists and per-person pricing. Selecting any fare highlights the chosen flight and raises a confirmation toast. The layout is mobile-first and collapses cleanly down to ~360px, with the rail stacking above the list and cards reflowing to a single column.
Illustrative UI only — fictional airline, not a real booking or flight system.