FlightStatus
A flight's status today: on time, delayed, boarding, in the air or cancelled, with the new times beside the old, gate changes and the baggage belt.
Demo
TP 1351 TAP Air Portugal · Thu 5 Nov 2026
Delayed 45 minLHRLondonDeparts 08:30, scheduled 07:45
LISLisbonArrives 11:05, scheduled 10:20
- Terminal
- 2
- Gate
- B42
was B36 - Arrival terminal
- 1
Updated 06:58 local time
QA 88 Quillo Air
In the airOPOPortoDeparts 22:41, scheduled 22:30
FRAFrankfurtArrives 01:52+1, scheduled 02:05
- Terminal
- 1
- Gate
- 12
- Arrival terminal
- 1
- Baggage belt
- 14
NW 512 Nordic Wings
CancelledCPHCopenhagenDeparts 18:05
LHRLondonArrives 19:00
Rebooked on NW 516 tomorrow at 07:10. Your new boarding pass is in the app.
DA 4
BoardingLHRDeparts 07:30
DXBArrives 18:25
- Terminal
- 4
- Gate
- 10
- Arrival terminal
- 3
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/flight_status.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/flight_status.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/flight_status.qwe
view FlightStatusDemo() = <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; align-items: start"> <FlightStatus id="flight-status-demo-late" flight="TP 1351" airline="TAP Air Portugal" date="2026-11-05" status="scheduled" from={{ code: "LHR", city: "London", terminal: "2", gate: "B42", gate_was: "B36" }} to={{ code: "LIS", city: "Lisbon", terminal: "1" }} departs="07:45" departs_new="08:30" arrives="10:20" arrives_new="11:05" updated="Updated 06:58 local time"/> <FlightStatus id="flight-status-demo-air" flight="QA 88" airline="Quillo Air" status="in_air" progress={62} from={{ code: "OPO", city: "Porto", terminal: "1", gate: "12" }} to={{ code: "FRA", city: "Frankfurt", terminal: "1", belt: "14" }} departs="22:30" departs_new="22:41" arrives="02:05" arrives_new="01:52" arrive_days={1}/> <FlightStatus id="flight-status-demo-off" flight="NW 512" airline="Nordic Wings" status="cancelled" from={{ code: "CPH", city: "Copenhagen" }} to={{ code: "LHR", city: "London" }} departs="18:05" arrives="19:00" note="Rebooked on NW 516 tomorrow at 07:10. Your new boarding pass is in the app."/> <FlightStatus id="flight-status-demo-ok" flight="DA 4" status="boarding" from={{ code: "LHR", terminal: "4", gate: "10" }} to={{ code: "DXB", terminal: "3" }} departs="07:30" arrives="18:25"/> </div>Source
ui/flight_status.qwe
// FlightStatus: where a flight stands today, as an airport's board or an airline's app// shows it: a status badge (on time, delayed by so long, boarding, departed, in the// air, landed, cancelled, diverted), each end's scheduled time and the new one when it// moved (the old struck through), the terminal, gate and baggage belt (a gate that// changed shows the old one struck), and while it flies, how far along it is.//// <FlightStatus flight="TP 1351" airline="TAP Air Portugal" status="delayed"// from={{ code: "LHR", city: "London", terminal: "2", gate: "B42", gate_was: "B36" }}// to={{ code: "LIS", city: "Lisbon", terminal: "1" }}// departs="07:45" departs_new="08:30" arrives="10:20" arrives_new="11:05" updated="Updated 06:58"/>// <FlightStatus flight="QA 88" status="in_air" progress={62} from={...} to={...} departs="22:30" arrives="02:05"// arrives_new="01:52" arrive_days={1}/>//// flight: its number; airline: whose. date: "YYYY-MM-DD" (shown "Thu 5 Nov 2026").// status: "scheduled" (default), "boarding", "departed", "in_air", "landed",// "delayed", "cancelled" or "diverted". from, to: { code, city?, terminal?, gate?,// gate_was? (a gate that changed), belt? (the baggage belt, at the arrival) }.// departs, arrives: the scheduled times ("07:45"); departs_new, arrives_new: the// expected or actual ones, when they differ. arrive_days: 1 when it lands the next// day. A "scheduled" flight leaving later than planned reads as delayed, by the// minutes between the two times, and one leaving earlier as early. progress: how far// along an "in_air" flight is (0 to 100). note: a line under it ("Diverted to Faro",// "Rebooked on TP 1353"). updated: when it was last checked. id, class.import ui/theme.qwe use ui_clslet flight_status_css = css`.ui-flstat { --ui-flstat-k: var(--ui-success); --ui-flstat-ks: var(--ui-success-soft); display: grid; gap: 16px; min-width: 0; max-width: 520px; padding: 18px 20px; border: 1px solid var(--ui-border); border-top: 4px solid var(--ui-flstat-k); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.4 var(--ui-font); font-variant-numeric: tabular-nums; }.ui-flstat-warning { --ui-flstat-k: var(--ui-warning); --ui-flstat-ks: var(--ui-warning-soft); }.ui-flstat-danger { --ui-flstat-k: var(--ui-danger); --ui-flstat-ks: var(--ui-danger-soft); }.ui-flstat-info { --ui-flstat-k: var(--ui-info); --ui-flstat-ks: var(--ui-info-soft); }.ui-flstat-accent { --ui-flstat-k: var(--ui-accent); --ui-flstat-ks: var(--ui-accent-soft); }.ui-flstat-muted { --ui-flstat-k: var(--ui-muted); --ui-flstat-ks: var(--ui-subtle); }.ui-flstat-head { display: flex; flex-wrap: wrap; align-items: start; justify-content: space-between; gap: 8px 12px; }.ui-flstat .ui-flstat-title { margin: 0; font-size: 18px; font-weight: 700; letter-spacing: -.01em; }.ui-flstat-sub { display: block; color: var(--ui-muted); font-size: 13px; font-weight: 400; letter-spacing: 0; }.ui-flstat-badge { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 11px; border-radius: 999px; background: var(--ui-flstat-ks); color: var(--ui-fg); font-size: 13px; font-weight: 650; white-space: nowrap; }.ui-flstat-badge::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-flstat-k); }.ui-flstat-live .ui-flstat-badge::before { animation: ui-flstat-pulse 1.6s ease-in-out infinite; }@keyframes ui-flstat-pulse { 50% { opacity: .3; } }.ui-flstat-ends { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }.ui-flstat-end { display: grid; gap: 2px; align-content: start; }.ui-flstat-end + .ui-flstat-end { text-align: right; }.ui-flstat-code { font-size: 28px; font-weight: 800; line-height: 1; letter-spacing: -.02em; }.ui-flstat-city { color: var(--ui-muted); font-size: 13px; }.ui-flstat-times { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; margin-top: 6px; }.ui-flstat-end + .ui-flstat-end .ui-flstat-times { justify-content: end; }.ui-flstat-time { font-size: 20px; font-weight: 700; }.ui-flstat-moved .ui-flstat-time { color: var(--ui-flstat-k); }.ui-flstat-was { color: var(--ui-muted); font-size: 14px; text-decoration: line-through; }.ui-flstat-days { margin-left: 1px; vertical-align: super; font-size: 11px; font-weight: 700; }.ui-flstat-cancelled .ui-flstat-time { color: var(--ui-muted); text-decoration: line-through; }.ui-flstat-track { position: relative; height: 4px; margin: 4px 12px; border-radius: 2px; background: var(--ui-border); }.ui-flstat-done { position: absolute; inset: 0 auto 0 0; border-radius: 2px; background: var(--ui-flstat-k); }.ui-flstat-plane { position: absolute; top: 50%; width: 22px; height: 22px; margin: -11px 0 0 -11px; color: var(--ui-flstat-k); }.ui-flstat-plane svg { display: block; width: 100%; height: 100%; transform: rotate(90deg); }.ui-flstat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 10px; margin: 0; padding: 12px 14px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-flstat-grid div { display: grid; grid-template-rows: 1fr auto; align-items: end; gap: 1px; min-width: 0; }.ui-flstat-grid dt { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }.ui-flstat-grid dd { margin: 0; font-size: 16px; font-weight: 700; }.ui-flstat-grid dd s { margin-left: 2px; color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-flstat-changed dd { color: var(--ui-warning); }.ui-flstat-note { margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-flstat-ks); font-size: 13px; }.ui-flstat-foot { margin: -6px 0 0; color: var(--ui-muted); font-size: 12px; }@media (prefers-reduced-motion: reduce) { .ui-flstat-live .ui-flstat-badge::before { animation: none; } }`let flight_status_days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]let flight_status_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// "HH:MM" as minutes since midnight, or null.fn flight_status_minutes(t: str?) -> int? { if t == null { return null } let m = re.find(t, "^(\\d{1,2}):(\\d{2})") m == null ? null : int(m.groups[0]) * 60 + int(m.groups[1])}// The minutes from one time of day to another, the shorter way round midnight (a// 23:50 flight leaving at 00:20 is 30 minutes late, not 23 hours early).fn flight_status_shift(from: str?, to: str?) -> int? { let a = flight_status_minutes(from) let b = flight_status_minutes(to) if a == null || b == null { return null } let d = b - a d > 720 ? d - 1440 : (d < -720 ? d + 1440 : d)}// Minutes as "45 min", "1 h 20 min", "2 h".fn flight_status_span(m: int) -> str { let h = floor(m / 60) let r = m % 60 h == 0 ? `${r} min` : (r == 0 ? `${h} h` : `${h} h ${r} min`)}// "YYYY-MM-DD" as "Thu 5 Nov 2026"; other text as it is.fn flight_status_date(d: str) -> str { if !date.valid(d, "date") { return d } let p = date.parts(d) `${flight_status_days[p.weekday - 1]} ${p.day} ${flight_status_months[p.month - 1]} ${p.year}`}// A cell of the details: a label and its value, with the old value struck when it changed.fn flight_status_cell(label: str, value, was) { if value == null { return null } let moved = was != null && str(was) != str(value) <div class={moved ? "ui-flstat-changed" : null}> <dt>{label}</dt> <dd>{value}{moved && [" ", <s><span.ui-sr>was </span>{was}</s>]}</dd> </div>}view FlightStatus(flight: str, from: map, to: map, departs: str, arrives: str, status: str = "scheduled", airline: str? = null, date: str? = null, departs_new: str? = null, arrives_new: str? = null, arrive_days: int = 0, progress = null, note: str? = null, updated: str? = null, id: str? = null, class: str? = null) { style global flight_status_css let late = flight_status_shift(departs, departs_new) ?? 0 let kind = status == "scheduled" && late > 0 ? "delayed" : status let text = match kind { "boarding" => "Boarding" "departed" => "Departed" "in_air" => "In the air" "landed" => "Landed" "delayed" => late > 0 ? `Delayed ${flight_status_span(late)}` : "Delayed" "cancelled" => "Cancelled" "diverted" => "Diverted" _ => late < 0 ? `Early ${flight_status_span(-late)}` : "On time" } let tone = match kind { "delayed" => "warning" "cancelled" | "diverted" => "danger" "boarding" => "accent" "departed" | "in_air" => "info" "landed" => "success" _ => "success" } let live = kind == "boarding" || kind == "in_air" let pct = progress != null ? (float(progress) < 0 ? 0 : (float(progress) > 100 ? 100 : float(progress))) : null let base = id ?? `flight-status-${re.replace(lower(flight), "[^a-z0-9]+", "")}` let end = (p: map, when: str, moved: str?, days: int, word: str) => { let changed = moved != null && moved != when <div class={ui_cls(["ui-flstat-end", changed && "ui-flstat-moved"])} data-end={word}> <span.ui-flstat-code>{p.code}</span> {p.city != null && <span.ui-flstat-city>{p.city}</span>} <span.ui-flstat-times> <span.ui-sr>{word == "from" ? "Departs" : "Arrives"} </span> <span.ui-flstat-time>{changed ? moved : when}{days > 0 && <sup.ui-flstat-days>+{days}</sup>}</span> {changed && <span.ui-flstat-was><span.ui-sr>, scheduled </span>{when}</span>} </span> </div> } <section class={ui_cls(["ui-flstat", `ui-flstat-${tone}`, `ui-flstat-${kind}`, live && "ui-flstat-live", class])} id={id} aria-labelledby={`${base}-title`} data-status={kind}> <div.ui-flstat-head> <h3.ui-flstat-title id={`${base}-title`}> {flight}{" "}<span.ui-flstat-sub>{join([airline, date != null ? flight_status_date(date) : null] | filter((x) => x != null), " · ")}</span> </h3> <span.ui-flstat-badge>{text}</span> </div> <div.ui-flstat-ends> {end(from, departs, departs_new, 0, "from")} {end(to, arrives, arrives_new, arrive_days, "to")} </div> {kind == "in_air" && pct != null && <div.ui-flstat-track role="img" aria-label={`${round(pct)}% of the way to ${to.city ?? to.code}`}> <span.ui-flstat-done style={`width: ${round(pct, 1)}%`}></span> <span.ui-flstat-plane style={`left: ${round(pct, 1)}%`}> <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><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.5l8 2.5Z"/></svg> </span> </div>} {(from.terminal != null || from.gate != null || to.terminal != null || to.belt != null) && <dl.ui-flstat-grid> {flight_status_cell("Terminal", from.terminal, null)} {flight_status_cell("Gate", from.gate, from.gate_was)} {flight_status_cell("Arrival terminal", to.terminal, null)} {flight_status_cell("Arrival gate", to.gate, to.gate_was)} {flight_status_cell("Baggage belt", to.belt, null)} </dl>} {note != null && <p.ui-flstat-note>{note}</p>} {updated != null && <p.ui-flstat-foot>{updated}</p>} </section>}Related
FlightResultA flight in search results: departure and arrival times, the time in the air, stops and layovers, cabin and bags, and the fare with Select.9 views · 9 downloadsBaggageSelectorChecked bags for each passenger with counters, what the first, second and third bag cost, the cabin allowance, the total and the saving against the airport.9 views · 8 downloadsCarRentalCardA hire car offer: class and model or similar, seats, bags, gearbox and air conditioning, mileage and fuel policy, supplier rating, pick-up and the price a day and in all.10 views · 11 downloadsEntryRequirementsVisa and entry rules for a passport: visa-free, travel authorisation, eVisa or visa required, the stay allowed, passport validity, documents and health rules.9 views · 8 downloadsFareCalendarA month of the cheapest fare each day: prices coloured cheap to dear, the lowest day marked, months with fares to move between, and a day to pick.8 views · 8 downloadsFareOptionsFare families side by side, Basic to Flex: the price a person and over the cheapest, what each includes, costs extra or leaves out, and the total for everyone.8 views · 11 downloads
Thread
Thread 0 posts
On FlightStatus: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.