Nº 176 of 520
Boarding pass with fold-out details
A horizontal airline boarding pass with notch cutouts, a plane gliding along the route arc, a QR stub, a details sheet that folds down, and an Add to Wallet button.
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 "Boarding pass with fold-out details" (piece card-boarding-pass) 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º 176 · "Boarding pass with fold-out details" · www.designlounge.live -->
# Boarding pass with fold-out details
> **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
The boarding pass screen of a fictional airline, Cirrus Air, for flight CA 417 from Kathmandu (KTM) to Bangkok (BKK). The pass is an 800px paper card on a pale sky ground. The left part shows the route in 64px codes with a dotted arc between them and an orange plane that glides halfway along it. Below sit gate, boarding time, group and seat. A dashed tear line with two round notches separates a 240px stub holding the passenger name and a QR code. Under the card, "Unfold details" folds a second sheet down from the card's bottom edge like folded paper, and "Add to Wallet" saves the pass with a short confirmation. The detail worth copying is the fold: the sheet rotates down from `rotateX(-88deg)` while its row grows from `0fr` to `1fr`, so it looks hinged rather than slid.
## Reference behaviour
1. First frame: pass centred, details folded away, button reads "Unfold details", wallet button reads "Add to Wallet". The plane starts at KTM and glides to the middle of the arc over 1600ms (quartic ease out). The solid part of the arc follows behind it.
2. Pointer enters the pass: the plane replays from the start to the middle over 1200ms.
3. The "On time" chip has a 6px dot that fades to 25% and back every 2.4s.
4. Click "Unfold details": `aria-expanded` becomes true, label "Fold details", chevron turns 180°. The details row grows from `0fr` to `1fr` over 420ms and the sheet rotates from `rotateX(-88deg)` to `none` over 520ms, both expo out, hinge at the top edge, perspective 1100px.
5. Click "Fold details": the reverse.
6. Click "Add to Wallet": the button shows a spinner and "Adding…" for 700ms, then turns green (`--ok`), shows a check and reads "In Wallet · Remove", `aria-pressed="true"`. The pass dips 10px and back over 620ms. The live region says the pass was added.
7. Click again while added: it returns to "Add to Wallet", `aria-pressed="false"`, live region "Removed from Wallet." Clicks during the 700ms busy state are ignored.
8. Reduced motion: plane is placed at the middle at once, no blink, no dip, no fold animation (the sheet appears and disappears instantly), wallet confirms immediately.
## Structure
```
1280 × 800, body grid centred, padding 32px 20px
.wrap width min(800px, 100%)
┌──────────────────────────── 560 ────────────────────────────┬●──── 240 ─────┐
│ ⟋ Cirrus Air BOARDING PASS · CA 417 · SUN 18 OCT [On time]┆ PASSENGER │
│ ┆ ANJALI RAI │
│ KTM ╭ ─ ─ ✈ ─ ─ ╮ BKK ┆ ┌──────────┐ │
│ Kathmandu 08:10 3h 15m · nonstop Bangkok 12:55 ┆ │ QR 136px │ │
│ ──────────────────────────────────────────────────────────── ┆ └──────────┘ │
│ GATE 14B │ BOARDS 07:35 │ GROUP 3 │ SEAT 23A ┆ Zone 3 · … │
└──────────────────────────────────────────────────────────────┴●──────────────┘
╰ details sheet, inset 14px each side, folds down: 4 × 2 grid ╯
[ ⌄ Unfold details ] [ ▭ Add to Wallet ]
```
- The pass is an `article` labelled "Boarding pass, Cirrus Air flight CA 417, Kathmandu to Bangkok". Grid `1fr 240px`, radius 18px.
- `section.main`: top row (brand, mono label, status chip), `.route` grid `auto 1fr auto`, then `dl.facts` with four pairs.
… 154 more lines. Copy or download for the full brief. Loading the source…Brief
194 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
The boarding pass screen of a fictional airline, Cirrus Air, for flight CA 417 from Kathmandu (KTM) to Bangkok (BKK). The pass is an 800px paper card on a pale sky ground. The left part shows the route in 64px codes with a dotted arc between them and an orange plane that glides halfway along it. Below sit gate, boarding time, group and seat. A dashed tear line with two round notches separates a 240px stub holding the passenger name and a QR code. Under the card, “Unfold details” folds a second sheet down from the card’s bottom edge like folded paper, and “Add to Wallet” saves the pass with a short confirmation. The detail worth copying is the fold: the sheet rotates down from rotateX(-88deg) while its row grows from 0fr to 1fr, so it looks hinged rather than slid.
Reference behaviour
- First frame: pass centred, details folded away, button reads “Unfold details”, wallet button reads “Add to Wallet”. The plane starts at KTM and glides to the middle of the arc over 1600ms (quartic ease out). The solid part of the arc follows behind it.
- Pointer enters the pass: the plane replays from the start to the middle over 1200ms.
- The “On time” chip has a 6px dot that fades to 25% and back every 2.4s.
- Click “Unfold details”:
aria-expandedbecomes true, label “Fold details”, chevron turns 180°. The details row grows from0frto1frover 420ms and the sheet rotates fromrotateX(-88deg)tononeover 520ms, both expo out, hinge at the top edge, perspective 1100px. - Click “Fold details”: the reverse.
- Click “Add to Wallet”: the button shows a spinner and “Adding…” for 700ms, then turns green (
--ok), shows a check and reads “In Wallet · Remove”,aria-pressed="true". The pass dips 10px and back over 620ms. The live region says the pass was added. - Click again while added: it returns to “Add to Wallet”,
aria-pressed="false", live region “Removed from Wallet.” Clicks during the 700ms busy state are ignored. - Reduced motion: plane is placed at the middle at once, no blink, no dip, no fold animation (the sheet appears and disappears instantly), wallet confirms immediately.
Structure
1280 × 800, body grid centred, padding 32px 20px
.wrap width min(800px, 100%)
┌──────────────────────────── 560 ────────────────────────────┬●──── 240 ─────┐
│ ⟋ Cirrus Air BOARDING PASS · CA 417 · SUN 18 OCT [On time]┆ PASSENGER │
│ ┆ ANJALI RAI │
│ KTM ╭ ─ ─ ✈ ─ ─ ╮ BKK ┆ ┌──────────┐ │
│ Kathmandu 08:10 3h 15m · nonstop Bangkok 12:55 ┆ │ QR 136px │ │
│ ──────────────────────────────────────────────────────────── ┆ └──────────┘ │
│ GATE 14B │ BOARDS 07:35 │ GROUP 3 │ SEAT 23A ┆ Zone 3 · … │
└──────────────────────────────────────────────────────────────┴●──────────────┘
╰ details sheet, inset 14px each side, folds down: 4 × 2 grid ╯
[ ⌄ Unfold details ] [ ▭ Add to Wallet ]
- The pass is an
articlelabelled “Boarding pass, Cirrus Air flight CA 417, Kathmandu to Bangkok”. Grid1fr 240px, radius 18px. section.main: top row (brand, mono label, status chip),.routegridauto 1fr auto, thendl.factswith four pairs.section.stublabelled “Scan at the gate”: passenger label and name,.qr(role="img",aria-label="Boarding QR code"), sequence text.- Notches:
::beforeand::afteron the pass, 28px circles in--bg, centred on the dashed line at top and bottom. .foldwrapper (id="fold-panel") holds adl.sheetof eight pairs..actions: two buttons, space-between.
Tokens
:root {
--bg: #dce5ea; /* sky ground; notch fill */
--paper: #fbfbf8; /* pass */
--paper-2: #f2f3ef; /* details sheet */
--ink: #0f2130;
--ink-2: #4f6170; /* labels */
--line: #d3dadf; /* hairlines, dashed tear line, arc track */
--accent: #ff5a1f; /* plane, group, brand mark, focus */
--ok: #1f7a55; /* on-time chip text, wallet added */
--sans: "Familjen Grotesk", system-ui, sans-serif;
--mono: "IBM Plex Mono", ui-monospace, monospace;
--r: 18px;
--shadow: 0 1px 0 rgba(15,33,48,.06), 0 24px 48px -24px rgba(15,33,48,.35);
--std: cubic-bezier(0.2, 0.7, 0.2, 1);
--expo: cubic-bezier(0.16, 1, 0.3, 1);
--t-fold-row: 420ms;
--t-fold-sheet: 520ms;
--t-fly: 1600ms;
}
Spacing: main padding 24px 30px 26px, gap 22px. Stub padding 24px 26px, gap 14px. Sheet padding 20px 26px 22px, gap 16px 20px.
Typography
| Role | Family | Size | Weight | Tracking | Notes |
|---|---|---|---|---|---|
| Brand | Familjen Grotesk | 17px | 700 | −.01em | |
| Labels (GATE, PASSENGER) | IBM Plex Mono | 11px | 500 | .12em | upper, --ink-2 |
| Airport code | Familjen Grotesk | 64px / 0.9 | 700 | −.04em | |
| City + time | Familjen 14px + Plex Mono 14px 500 | time in --ink | |||
| Duration | IBM Plex Mono | 12px | 500 | 0 | --ink-2 |
| Fact value | Familjen Grotesk | 28px | 600 | −.02em | group in --accent |
| Passenger | Familjen Grotesk | 18px | 700 | .02em | upper |
| Sequence | IBM Plex Mono | 12px / 1.5 | 500 | 0 | |
| Sheet value | Familjen Grotesk | 15px | 500 | 0 | sub-line 13px --ink-2 |
| Buttons | Familjen Grotesk | 15px | 600 | 0 |
Codes, times and identifiers are mono. Words are grotesk.
Motion
| Thing | Trigger | Property | From → to | Duration / easing |
|---|---|---|---|---|
| Plane | load, pointerenter | position on path, rotation | t 0 → 0.5 | 1600ms (1200ms replay), 1 − (1 − k)^4 |
| Arc fill | with plane | stroke-dashoffset | L → L × 0.5 | same |
| Status dot | always | opacity | 1 → .25 → 1 | 2.4s, standard, infinite |
| Fold row | toggle | grid-template-rows | 0fr ↔ 1fr | 420ms expo out |
| Fold sheet | toggle | transform | rotateX(−88deg) ↔ none | 520ms expo out |
| Chevron | toggle | rotate | 0 ↔ 180deg | 320ms expo out |
| Wallet busy | click | spinner rotate | 0 → 1 turn | 700ms linear, loop until done |
| Pass dip | added | transform | 0 → translateY(10px) scale(.985) → 0 | 620ms expo out |
| Button press | active | scale | 1 → .97 | 120ms standard |
Reduced motion: no plane flight (placed at 0.5), no blink, no dip, no fold transitions, no wait on the wallet.
States
- Fold button: resting is a 1px inset ring at 22% ink; hover adds
rgba(255,255,255,.55);aria-expanded="true"flips the chevron and the label. - Wallet resting:
--inkfill, white text, 208px min width. Hover#1c3448. Busy: spinner replaces the icon, label “Adding…”, clicks ignored. Added:--okfill, check icon, “In Wallet · Remove”,aria-pressed="true". - Focus-visible: 2px
--accentoutline, offset 3px, on both buttons. - Status chip: “On time” in
--okon#e3f1ea. A delayed flight would swap to the accent on a pale orange; not shown here. - The QR is decorative data. There is no loading or error state for it in this demo.
Accessibility
- The pass is an
articlewith a full sentence label. The stub section is labelled “Scan at the gate”. - Facts and details are
dllists, so screen readers pair label and value. - The QR container is
role="img"with a label; the arc and plane arearia-hidden. - Fold button:
aria-expandedandaria-controls="fold-panel". - Wallet button:
aria-pressed. A polite live region confirms “Boarding pass added to Wallet. It will surface at Kathmandu airport.” and “Removed from Wallet.” - Tab order: fold, then wallet. The pass itself is not focusable; it has no action.
- Contrast:
--inkon--paper16:1;--ink-2labels 6:1;--accenton paper is about 3:1, so it is used only for 28px+ values and the plane, never body text. - Buttons are 44px tall.
Responsive rules
- ≥1024: as specified, 800px wide.
- 768: the pass still fits at 728px; columns shrink, codes stay 64px.
- ≤720: the pass stacks. Main on top, stub below (254px tall), dashed line becomes horizontal and the notches move to the left and right ends of it. Codes drop to 40px, city text 13px, arc height 56px, duration hidden. Facts become a 2 × 2 grid. The stub becomes
1fr autowith the QR on the right. The details sheet becomes two columns. The wallet button grows to fill the row. - 375: no horizontal scroll; the mono label in the top row is hidden and the chip moves right.
Acceptance checklist
Always
- Main part and stub are separated by a dashed line with two round notches that sit exactly on it, in both layouts.
- The plane moves along the arc and rotates to its tangent; the travelled part of the arc is solid, the rest dotted.
- Unfold grows the space and rotates the sheet from its top edge; fold reverses it.
- Fold button exposes
aria-expandedandaria-controls. - Wallet has resting, busy, added states and can be undone;
aria-pressedreflects it. - A live region confirms wallet changes.
- Codes and identifiers are monospaced; values are at least 28px in the facts row.
- Stacks to one column at ≤720px with no horizontal scroll at 375px.
- Reduced motion removes the flight, blink, dip and fold animation.
This demo
- Cirrus Air, CA 417, Sun 18 Oct, KTM 08:10 → BKK 12:55, 3h 15m nonstop.
- Gate 14B, Boards 07:35, Group 3 (orange), Seat 23A. Passenger Anjali Rai, Zone 3 · Economy Light, SEQ 087 · QX7RLM.
- Details: Terminal 1 International, Gate closes 07:55, 1 × 23 kg checked, Vegetarian, ticket 217 4402 938156, booking QX7RLM, A321neo, Stratus Silver.
- Pass 800px wide, stub 240px, radius 18px, notches 28px.
- Wallet busy for 700ms then green
#1f7a55.
Implementation notes
The fold is two transitions on two elements. The wrapper animates its row; the sheet rotates from its top edge:
.fold { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 420ms var(--expo); perspective: 1100px; margin: 0 14px; }
.fold.open { grid-template-rows: 1fr; }
.fold > div { overflow: hidden; }
.sheet { transform-origin: 50% 0; transform: rotateX(-88deg); transition: transform 520ms var(--expo); }
.open .sheet { transform: none; }
Do not animate height: auto with JS measurements, and do not use max-height hacks; grid-template-rows interpolates cleanly.
The plane follows the SVG path with getPointAtLength, scaled because the SVG uses preserveAspectRatio="none":
function place(t) {
const p = track.getPointAtLength(L * t), q = track.getPointAtLength(Math.min(L, L * t + 1));
const r = arc.getBoundingClientRect(), sx = r.width / 300, sy = r.height / 78;
const a = Math.atan2((q.y - p.y) * sy, (q.x - p.x) * sx) * 180 / Math.PI;
plane.style.transform = `translate(${p.x * sx}px, ${p.y * sy}px) rotate(${a + 90}deg)`;
done.style.strokeDashoffset = L * (1 - t);
}
The plane is an HTML-positioned SVG on top of the arc SVG, not inside it, so it does not stretch with the arc. Make sure your generic .arc svg { inset: 0; width: 100% } rule does not also hit the plane.
The QR is a 25 × 25 module grid drawn as a single path (M x y h1 v1 h-1z per module) with three 7 × 7 finder squares at three corners and shape-rendering: crispEdges. In a product, render the real payload with a QR library instead.
Common mistakes:
- Notches drawn on only one part, or not centred on the dashed line after the layout stacks.
- Making the whole pass a giant button. It has no single action.
- Putting the airport codes in the mono face. Codes are the display; mono is for small identifiers.
- A wallet button with no undo.
- Using the accent orange for body text.
Details
- Palette
- Type
- Familjen Grotesk · IBM Plex Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
Studied
Icon MuseumLet the icons be the only colour. Give each one a shelf, a lift on hover, and a quiet label.
All sourcesOn the shelves