Nº 248 of 520
Flight arrival split-flap widget
An airport-signage flight widget: a plane rides a route arc, times and gate flip on split-flap cells, and a delay toggle re-flips the board.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Flight arrival split-flap widget" (piece widget-flight-arrival) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 248 · "Flight arrival split-flap widget" · www.designlounge.live -->
# Flight arrival split-flap widget
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A flight status widget for a fictional airline, "Aerovale", tracking AV 218 from Kathmandu (KTM) to Bangkok (BKK). It borrows from airport wayfinding: a signage-yellow band, black board, condensed capitals, and split-flap cells for times, gate, terminal, belt, and status. A plane icon rides a dotted great-circle arc; the flown part is solid yellow. A "Simulate delay" switch under the widget pushes the arrival 45 minutes later and moves the gate. Every changed cell flips through the alphabet to its new character, so the board itself shows that something changed. The detail worth copying: the flaps are four half-height spans with two Web Animations, and the status colour is the only colour that changes; everything else stays signage black and yellow.
## Reference behaviour
1. First frame: the widget is centred on a dark floor with a faint 96px grid. Band reads "ARRIVALS · AEROVALE AV 218 · ● LIVE". The plane sits 63% along the arc (117 of 185 minutes flown). Flaps: Scheduled `14:35`, Estimated `14:35`, Gate `B7`, Term `1`, Belt `4`, Status `ON TIME` in green. Right of status, "LANDS IN 1h 08m" in 40px condensed. Footer: "Cruising at 11,600 m over the Bay of Bengal" and "Seat 14A · Mina Gurung". Under the widget: the switch and "Local 13:27".
2. On load every cell flips from blank to its value, staggered 40ms per cell inside each field.
3. A simulated clock advances one minute every 1.5s. Each minute: the plane and the solid arc move forward (600ms expo out), "Lands in" counts down, the clock text updates. The LIVE dot blinks on a 2s step.
4. Toggle "Simulate delay" on: the switch thumb slides and turns orange. Estimated flips to `15:20` and turns yellow, Gate flips to `C2`, Status flips to `DELAYED` in orange. Progress is now measured against the later arrival, so the plane eases back along the arc. The footer reads "Holding east of BKK for runway slot". A live region says "AV 218 delayed 45 minutes. New arrival 15:20, gate C2."
5. Toggle off: everything flips back to the on-time values and the region says "AV 218 back on time. Arrival 14:35, gate B7."
6. When the clock reaches the arrival: plane at BKK, arc fully yellow, Status flips to `LANDED`, label reads "LANDED" with the arrival time, footer "Taxiing to gate B7". Five seconds later the clock resets to 13:27 and the board flips back.
7. A cell flips at most 12 steps toward its target, 120ms per step (60ms top half down, 60ms bottom half up). If a new value arrives mid-flip, the cell finishes and then flips on to the newest value.
8. Reduced motion: cells change instantly, the plane and arc jump, the LIVE dot does not blink, the switch snaps. The clock still runs.
## Structure
```
1280 × 800 · body grid centred, padding 32px 16px · widget max 640px
┌─────────────────────────────────────────────────────────────┐
│ ✈ ARRIVALS AEROVALE AV 218 ● LIVE │ band 48px, yellow
├─────────────────────────────────────────────────────────────┤
│ KTM ╭──────✈ · · · · ╮ BKK │ codes 48px
│ Kathmandu ● 3H 05M · 1,920 KM ○ Bangkok │ arc viewBox 400×110
├─────────────────────────────────────────────────────────────┤ 1px rule
│ SCHEDULED ESTIMATED GATE TERM BELT │ labels 10px
│ [1][4]:[3][5] [1][4]:[3][5] [B][7] [1] [4] │ cells 30×44
│ STATUS LANDS IN │
│ [O][N][ ][T][I][M][E][ ] 1h 08m (40px) │
├─────────────────────────────────────────────────────────────┤
│ Cruising at 11,600 m over… Seat 14A · Mina G. │ footer 12px
└─────────────────────────────────────────────────────────────┘
( ◯— SIMULATE DELAY ) Local 13:27
```
… 192 more lines. Copy or download for the full brief. Loading the source…Brief
232 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A flight status widget for a fictional airline, “Aerovale”, tracking AV 218 from Kathmandu (KTM) to Bangkok (BKK). It borrows from airport wayfinding: a signage-yellow band, black board, condensed capitals, and split-flap cells for times, gate, terminal, belt, and status. A plane icon rides a dotted great-circle arc; the flown part is solid yellow. A “Simulate delay” switch under the widget pushes the arrival 45 minutes later and moves the gate. Every changed cell flips through the alphabet to its new character, so the board itself shows that something changed. The detail worth copying: the flaps are four half-height spans with two Web Animations, and the status colour is the only colour that changes; everything else stays signage black and yellow.
Reference behaviour
- First frame: the widget is centred on a dark floor with a faint 96px grid. Band reads “ARRIVALS · AEROVALE AV 218 · ● LIVE”. The plane sits 63% along the arc (117 of 185 minutes flown). Flaps: Scheduled
14:35, Estimated14:35, GateB7, Term1, Belt4, StatusON TIMEin green. Right of status, “LANDS IN 1h 08m” in 40px condensed. Footer: “Cruising at 11,600 m over the Bay of Bengal” and “Seat 14A · Mina Gurung”. Under the widget: the switch and “Local 13:27”. - On load every cell flips from blank to its value, staggered 40ms per cell inside each field.
- A simulated clock advances one minute every 1.5s. Each minute: the plane and the solid arc move forward (600ms expo out), “Lands in” counts down, the clock text updates. The LIVE dot blinks on a 2s step.
- Toggle “Simulate delay” on: the switch thumb slides and turns orange. Estimated flips to
15:20and turns yellow, Gate flips toC2, Status flips toDELAYEDin orange. Progress is now measured against the later arrival, so the plane eases back along the arc. The footer reads “Holding east of BKK for runway slot”. A live region says “AV 218 delayed 45 minutes. New arrival 15:20, gate C2.” - Toggle off: everything flips back to the on-time values and the region says “AV 218 back on time. Arrival 14:35, gate B7.”
- When the clock reaches the arrival: plane at BKK, arc fully yellow, Status flips to
LANDED, label reads “LANDED” with the arrival time, footer “Taxiing to gate B7”. Five seconds later the clock resets to 13:27 and the board flips back. - A cell flips at most 12 steps toward its target, 120ms per step (60ms top half down, 60ms bottom half up). If a new value arrives mid-flip, the cell finishes and then flips on to the newest value.
- Reduced motion: cells change instantly, the plane and arc jump, the LIVE dot does not blink, the switch snaps. The clock still runs.
Structure
1280 × 800 · body grid centred, padding 32px 16px · widget max 640px
┌─────────────────────────────────────────────────────────────┐
│ ✈ ARRIVALS AEROVALE AV 218 ● LIVE │ band 48px, yellow
├─────────────────────────────────────────────────────────────┤
│ KTM ╭──────✈ · · · · ╮ BKK │ codes 48px
│ Kathmandu ● 3H 05M · 1,920 KM ○ Bangkok │ arc viewBox 400×110
├─────────────────────────────────────────────────────────────┤ 1px rule
│ SCHEDULED ESTIMATED GATE TERM BELT │ labels 10px
│ [1][4]:[3][5] [1][4]:[3][5] [B][7] [1] [4] │ cells 30×44
│ STATUS LANDS IN │
│ [O][N][ ][T][I][M][E][ ] 1h 08m (40px) │
├─────────────────────────────────────────────────────────────┤
│ Cruising at 11,600 m over… Seat 14A · Mina G. │ footer 12px
└─────────────────────────────────────────────────────────────┘
( ◯— SIMULATE DELAY ) Local 13:27
main.wrap: grid, gap 20px, max-width 640px.article.w[aria-labelledby]: radius 20px,overflow: hidden, 1px black border.header.band: icon, “Arrivals” label, flight name (the article’s label), LIVE dot..route: three-column gridauto minmax(0,1fr) auto, the arc SVG in the middle withrole="img".dl.facts: flex-wrap, gap 20px 28px. Each fact is adivwithdtanddd. Eachddholds the cells and carries anaria-labelwith the plain value (“Gate C2”). Cells arearia-hidden.- Lands-in fact:
margin-left: auto, right-aligned, plain text, not flaps. footer.foot: note and passenger.- Below:
button.tog[aria-pressed]with a 32 × 18 switch, and the clock span.
Tokens
:root {
--floor: #24231f; /* page */
--board: #121211; /* widget */
--cell: #1f1f1d; /* flap bottom half */
--cell-hi: #2a2a27; /* flap top half, catches light */
--seam: #050505; /* 1px split line */
--ink: #f4f1e6; /* flap characters, codes */
--ink-2: #b9b5a8; /* city names, footer */
--ink-3: #8e8a7e; /* labels, dotted arc */
--line: #2e2d29; /* rules */
--sign: #ffc72c; /* band, flown arc, focus, changed estimate */
--sign-ink: #141310; /* text on the band */
--ok: #8fd694; /* ON TIME */
--warn: #ff7a45; /* DELAYED, switch on */
--cond: "Barlow Condensed", system-ui, sans-serif;
--mono: "Overpass Mono", ui-monospace, monospace;
--cw: 30px; --ch: 44px; --cf: 30px; /* cell width, height, glyph */
--cell-gap: 3px;
--r-widget: 20px; --r-cell: 4px;
--std: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
--t-flip-half: 60ms; /* ×2 per step */
--t-stagger: 40ms; /* per cell */
--t-plane: 600ms;
--tick: 1500ms; /* one simulated minute */
}
Widget shadow: 0 40px 80px -40px rgba(0,0,0,.9), inset 0 1px 0 rgba(255,255,255,.05). Floor: two 96px grids of 1px lines at 3.5% white over a radial #2f2d27 → --floor.
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Band label, flight | Barlow Condensed | 20px | 700 / 600 | 1 | 0.06em / 0.04em | UPPER |
| LIVE | Overpass Mono | 11px | 600 | 1 | 0.1em | UPPER |
| Airport code | Barlow Condensed | 48px | 700 | 0.9 | 0.01em | UPPER |
| City | Overpass Mono | 12px | 400 | 1.4 | 0 | Title |
| Arc caption | Overpass Mono | 10px | 400 | — | 0.06em | UPPER |
| Fact label | Overpass Mono | 10px | 600 | 1.4 | 0.14em | UPPER |
| Flap glyph | Barlow Condensed | 30px | 600 | 44px | 0 | UPPER |
| Lands in | Barlow Condensed | 40px | 700 | 44px | 0.01em | tabular |
| Footer | Overpass Mono | 12px | 400 | 1.4 | 0 | Sentence |
| Switch label | Overpass Mono | 12px | 600 | 1 | 0.06em | UPPER |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
| Flap top half | value change | rotateX | 0 → −90° | 60ms | ease-in | instant swap |
| Flap bottom half | after top | rotateX | 90° → 0 | 60ms, delay 60ms | ease-out | instant swap |
| Cells in a field | value change | start time | +40ms each | — | — | none |
| Plane | each minute, delay toggle | translate + rotate along arc | prev → next | 600ms | --expo | instant |
| Flown arc | same | stroke-dashoffset | prev → next | 600ms | --expo | instant |
| LIVE dot | always | opacity | 1 → .2 | 2s | steps(1) | static |
| Switch thumb | toggle | translateX | 0 → 14px | 200ms | --expo | instant |
| Switch track | toggle | background | #3a3934 → --warn | 200ms | --std | instant |
Cells flip through the sequence " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", starting after the current character, capped at 12 steps; step 12 lands on the target regardless.
States
- On time: status green
ON TIME, estimate in--ink, gate B7. - Delayed:
.delayedon the widget. Status orangeDELAYED, estimate yellow15:20, gate C2, footer note changes, switch on. - Landed: status
LANDED, label “Landed” with the time, plane at the destination dot. - Cell flipping: the cell is busy; new targets queue as
next. - Switch hover: background 6% → 10% white.
- Focus-visible: 2px
--signoutline, 3px offset, on the switch. - Loading: a real product shows every cell blank (
' ') and the plane hidden until data arrives; then the load flip runs. - Error: status flips to
NO DATAin--ink-3, Lands in shows an em dash. Not shown in the demo.
Accessibility
- The widget is an
articlelabelled by the flight name. - Each
ddcarries anaria-labelwith the readable value (“Estimated 15:20”, “Gate C2”, “Status delayed 45 minutes”). The cells arearia-hidden, so screen readers never hear “one four three five”. - The arc is
role="img",aria-label="Flight progress"; the progress itself is spoken by “Lands in”. - A polite live region announces the delay, the recovery, and the landing. It does not announce each minute.
- The switch is a
buttonwitharia-pressed; its label is “Simulate delay”. - Status colour is never alone: the word changes too.
- Contrast:
--inkon--cell15:1.--okon--cell10:1.--warnon--cell6.4:1.--sign-inkon--sign12:1.--ink-3labels on--board5.5:1. - Switch hit target 40px tall.
Responsive rules
- ≥ 1024: widget 640px. Facts in two rows: times, gate, term, belt on the first; status and Lands in on the second.
- 768: same; the widget is narrower than the viewport, nothing changes.
- < 640: cells shrink to 22 × 34 with a 23px glyph. Codes 34px. Paddings 16px. The “Arrivals” word and the arc caption hide. Facts wrap to three rows; Lands in drops to the left at 30px. Footer stacks.
- The widget never exceeds the viewport: width 100%, max 640px,
overflow-x: hiddenon body as a guard. - Cell size lives in three custom properties, so one media query rescales every flap.
Acceptance checklist
Always
- Every changing value is drawn in split-flap cells with a 1px seam at 50% height.
- A changed cell flips through intermediate characters, 120ms per step, at most 12 steps.
- Cells within one field stagger by 40ms.
- A value arriving mid-flip is queued, not dropped.
- The plane rotates to the arc tangent; the flown part of the arc is solid, the rest dotted.
- A delay changes estimate, gate, status word, status colour, and the note together.
- Fields have plain-language
aria-labels; cells arearia-hidden. - A polite live region announces delay, recovery, and landing only.
- Reduced motion: no flips, no blinking, no plane glide; values still update.
- No horizontal scroll at 375px.
This demo
- Aerovale AV 218, KTM Kathmandu → BKK Bangkok, 3H 05M · 1,920 KM.
- Scheduled 14:35, on-time gate B7, delayed 15:20 at gate C2, Term 1, Belt 4.
- Band
#ffc72c48px; board#121211; cells 30 × 44. - ON TIME
#8fd694, DELAYED#ff7a45, delayed estimate#ffc72c. - Clock starts at 13:27 and advances one minute per 1.5s.
Implementation notes
A flap cell is four half-height spans that print their character with attr(). The two static halves show “new on top, old on bottom”; the two flaps rotate over them.
.c { position: relative; width: var(--cw); height: var(--ch); perspective: 200px; }
.c span { position: absolute; left: 0; right: 0; height: 50%; overflow: hidden; backface-visibility: hidden; background: inherit; }
.c span::before { content: attr(data-ch); position: absolute; inset-inline: 0; height: var(--ch); line-height: var(--ch); text-align: center; }
.c .t, .c .ft { top: 0; transform-origin: 50% 100%; background: var(--cell-hi); }
.c .b, .c .fb { bottom: 0; transform-origin: 50% 0; }
.c .b::before, .c .fb::before { top: calc(var(--ch) / -2); } /* show the lower half of the glyph */
.c .ft, .c .fb { z-index: 2; visibility: hidden; }
.c::after { content: ""; position: absolute; inset-inline: 0; top: 50%; height: 1px; background: var(--seam); z-index: 3; }
function flipOnce(c, from, to) {
const [t, b, ft, fb] = c.children;
t.dataset.ch = to; b.dataset.ch = from; ft.dataset.ch = from; fb.dataset.ch = to;
ft.style.visibility = fb.style.visibility = 'visible';
ft.animate([{ transform: 'rotateX(0)' }, { transform: 'rotateX(-90deg)' }], { duration: 60, easing: 'ease-in', fill: 'forwards' });
return fb.animate([{ transform: 'rotateX(90deg)' }, { transform: 'rotateX(0)' }],
{ duration: 60, delay: 60, easing: 'ease-out', fill: 'forwards' })
.finished.then(() => { setCh(c, to); ft.style.visibility = fb.style.visibility = 'hidden'; });
}
Progress is time, not distance. p = (now − departure) / (estimated − departure). When the delay adds 45 minutes, p drops and the plane eases back. That is honest: the widget re-estimates.
Queue, don’t interrupt. Toggling the switch twice quickly would otherwise leave half a board in the old state:
async function flipTo(c, to) {
if (c.busy) { c.next = to; return; }
c.busy = true;
/* step through SEQ with await flipOnce(...) */
c.busy = false;
if (c.next) { const n = c.next; c.next = null; flipTo(c, n); }
}
Common mistakes:
- Flipping the whole cell as one 3D card. Real split-flaps hinge at the middle; the top half falls and reveals the new bottom.
- Reading the digits aloud cell by cell. Label the field, hide the cells.
- Using yellow for everything. Yellow is signage (band, flown arc, changed estimate); green and orange are status only.
- Animating the plane with a fixed-duration CSS path animation. It can’t go backwards when the estimate changes.
- Letting the cells reflow at 375px. Shrink them with the three size variables instead.
Rebuild order:
- Floor, widget shell, band.
- Route row with codes and the arc SVG;
place(p)for the plane and dashoffset. - The cell component and
board(name, text, width)that creates and flips cells. - Facts list with labels and the Lands-in readout.
- Simulated clock, landed state, reset.
- Delay switch, colour states, live region.
- Mobile sizes and reduced motion.
Details
- Palette
- Type
- Barlow Condensed · Overpass Mono
- Motion
- Rich motion
- Build
- Multi-step
- Tags
On the shelves