Covers: color tokens, typography scale, spacing grid, 13 components (nav sidebar/bottom, buttons, status chips, stat cards, data table, forms, segmented toggle, toasts, empty states, skeletons, progress), page-by-page specs for all 5 views, motion guidelines, Lucide icon catalogue, and Tailwind v4 implementation checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
35 KiB
Flight Radar — Design System
Version 1.0 · February 2026
This document is the authoritative reference for the visual design and UX of the Flight Radar web application. It covers the full design system: principles, tokens, components, and page-level specifications — ready for direct implementation in Tailwind CSS v4 + React.
Table of Contents
- Current State Audit
- Design Principles
- Color System
- Typography
- Spacing & Layout Grid
- Elevation & Shadow
- Component Library
- Page Specifications
- Motion & Animation
- Icons
- Implementation Checklist
1. Current State Audit
What's broken today
| Area | Problem | Impact |
|---|---|---|
| Navigation | Nav links butted together with no gaps: renders as DashboardScansAirportsLogs |
Unreadable, looks broken |
| Dashboard stats | Five identical white boxes with no accent, icon, or trend | Zero visual hierarchy |
| Scan list items | Status badge glued to destination: BDS → FMM,DUScompleted |
Parsing required by user |
| Form | Flat input fields, no grouping, no visual section breaks | Feels like a raw HTML form |
| Mode toggle | Full-width button pair looks like a toggle switch gone wrong | Confusing affordance |
| Airports page | Empty white canvas with an input box — no state, no branding | Dead end feeling |
| Logs page | Wall of monospaced text with no log-level differentiation | No scanability |
| Mobile | Nav links overflow and collapse poorly | Unusable on small screens |
| Loading states | Plain text "Loading…" or invisible spinner | No feedback |
| Empty states | Plain text only, no illustration or call to action | Abandoned feeling |
| Colors | Two blues + basic status colours, no system | Incoherent palette |
| Typography | Browser default font, no size scale | Low credibility |
| Branding | ✈️ emoji as logo | Not scalable, not professional |
What's good and must be kept
- Tailwind CSS v4 already installed — just needs design tokens
- React Router working, page structure sound
- Component boundaries (AirportSearch, Layout, pages) are clean
- Status badge color logic is correct
- Auto-refresh on running scans is a great UX feature
- Expandable flight rows in the route table are the right pattern
2. Design Principles
1. Data first, chrome second
Flight data is complex (many numbers, dates, airports). The UI must recede and let data breathe. Avoid decorative elements that compete with content.
2. Material Design 3 — adapted, not copied
Use MD3's color system (tonal palettes, surface roles), elevation model (tonal elevation, not drop shadows), and component shapes (rounded corners). Ditch components that don't fit a desktop tool (FABs, bottom nav for desktop).
3. Mobile-first, desktop-optimised
Start every layout from 320 px. Desktop adds a sidebar and wider data tables — it doesn't redesign the page.
4. Instant feedback everywhere
Every async action shows a skeleton, spinner, or progress indicator. Every destructive or slow action is confirmed. Success and error are always communicated via toast.
5. One primary action per view
Each page has exactly one primary CTA (blue filled button). Everything else is secondary or ghost.
3. Color System
Brand palette (MD3 tonal palette — primary hue: blue 220°)
Primary #1A73E8 (Google Blue — familiar, trustworthy for a data tool)
On Primary #FFFFFF
Primary Cont. #D2E3FC (primary container — used for active nav, chips)
On Primary Cont.#003E8C
Secondary #0F9D58 (green — price / success / confirmed flights)
On Secondary #FFFFFF
Secondary Cont. #C8E6C9
Tertiary #F4B400 (amber — warnings, pending states)
On Tertiary #3B2A00
Tertiary Cont. #FFF0C0
Error #D93025 (red — failed, error)
Error Cont. #FDECEA
Surface #F8F9FA (page background — very light grey)
Surface 1 #FFFFFF (card background — level 1)
Surface 2 #F1F3F4 (table row alternate / subtle separators)
Surface Variant #E8EAED (input backgrounds, chip backgrounds)
On Surface #202124 (primary text)
On Surface Var. #5F6368 (secondary text, labels, metadata)
Outline #DADCE0 (borders, dividers)
Outline Var. #F1F3F4 (very subtle separators)
Semantic status colours
| Status | Background | Text | Border | Usage |
|---|---|---|---|---|
completed |
#E6F4EA |
#137333 |
#A8D5B5 |
Scan finished, route confirmed |
running |
#E8F0FE |
#1557B0 |
#A8C7FA |
Scan in progress |
pending |
#FEF7E0 |
#7A5200 |
#F9D659 |
Queued, not started |
failed |
#FDECEA |
#A50E0E |
#F5C6C6 |
Error state |
Dark mode (implement via CSS variables)
/* light (default) */
:root {
--color-bg: #F8F9FA;
--color-surface: #FFFFFF;
--color-surface-2: #F1F3F4;
--color-on-surface: #202124;
--color-on-surface-var: #5F6368;
--color-outline: #DADCE0;
--color-primary: #1A73E8;
--color-secondary: #0F9D58;
}
/* dark */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1C1B1F;
--color-surface: #2C2B30;
--color-surface-2: #36343B;
--color-on-surface: #E6E1E5;
--color-on-surface-var: #938F99;
--color-outline: #49454F;
--color-primary: #A8C7FA;
--color-secondary: #81C995;
}
}
4. Typography
Font stack
font-family: 'Google Sans', 'Roboto', system-ui, -apple-system, sans-serif;
font-family-mono: 'Roboto Mono', 'JetBrains Mono', monospace; /* logs, IATA codes */
Load from Google Fonts:
Google Sans(300, 400, 500) +Roboto Mono(400)
Type scale (MD3 aligned)
| Token | Size | Weight | Line height | Usage |
|---|---|---|---|---|
display-sm |
36px / 2.25rem | 400 | 44px | Page hero (empty states) |
headline-lg |
32px / 2rem | 400 | 40px | — |
headline-md |
28px / 1.75rem | 400 | 36px | Page title |
headline-sm |
24px / 1.5rem | 400 | 32px | Section title |
title-lg |
22px / 1.375rem | 500 | 28px | Card title |
title-md |
16px / 1rem | 500 | 24px | List item title, form label |
title-sm |
14px / 0.875rem | 500 | 20px | Table header, tab label |
body-lg |
16px / 1rem | 400 | 24px | Body copy, descriptions |
body-md |
14px / 0.875rem | 400 | 20px | Secondary body, list metadata |
body-sm |
12px / 0.75rem | 400 | 16px | Captions, helper text |
label-lg |
14px / 0.875rem | 500 | 20px | Button labels |
label-md |
12px / 0.75rem | 500 | 16px | Badge labels, chip labels |
label-sm |
11px / 0.6875rem | 500 | 16px | Overlines, micro labels |
mono-md |
13px / 0.8125rem | 400 | 20px | IATA codes, log messages |
5. Spacing & Layout Grid
Spacing scale (4px base)
1 → 4px (tight gaps: icon-to-text, badge padding)
2 → 8px (small gaps: form field helper text)
3 → 12px (medium-small: list item inner padding)
4 → 16px (base unit: card inner padding mobile)
5 → 20px (—)
6 → 24px (card inner padding desktop, section gaps)
8 → 32px (between major sections)
10 → 40px (—)
12 → 48px (page top padding)
16 → 64px (large empty state illustrations)
Layout grid
Mobile (< 600px)
- 1 column, 16px margins, 16px gutters
- Full-width cards
- Bottom navigation bar (56px)
Tablet (600–1024px)
- 8 columns, 24px margins, 24px gutters
- Side rail navigation (72px collapsed)
- Cards: 2-column grid for stats
Desktop (> 1024px)
- 12 columns, 24px margins, 24px gutters
- Fixed sidebar navigation (256px)
- Max content width: 1280px
- Cards: 3–5 column grid for stats
Sidebar layout (desktop)
┌──────────┬────────────────────────────────┐
│ │ Top bar (64px, search + user) │
│ Sidebar ├────────────────────────────────┤
│ (256px) │ │
│ │ Main content area │
│ Logo │ max-w: 1024px, centered │
│ Nav │ px: 24px │
│ items │ │
│ │ │
│ ────── │ │
│ Logs │ │
└──────────┴────────────────────────────────┘
6. Elevation & Shadow
MD3 uses tonal elevation (surface tint) as primary depth cue, with subtle shadows for resting state only.
| Level | Shadow | Tint overlay | Usage |
|---|---|---|---|
| 0 | none | 0% | Page background |
| 1 | 0 1px 2px rgba(0,0,0,.08) |
5% primary | Cards (resting) |
| 2 | 0 2px 6px rgba(0,0,0,.10) |
8% primary | Dropdowns, popovers |
| 3 | 0 4px 12px rgba(0,0,0,.12) |
11% primary | Modals, dialogs |
| 4 | 0 6px 20px rgba(0,0,0,.14) |
12% primary | Navigation drawer |
7. Component Library
7.1 Navigation
Sidebar (desktop ≥ 1024px)
Width: 256px (expanded) / 72px (collapsed — future)
Background: Surface (white)
Border-right: 1px solid Outline (#DADCE0)
Logo area (64px tall):
✈ icon (24px, Primary blue) + "Flight Radar" (title-lg, On Surface)
Nav item (48px tall, full width):
- Inactive: transparent bg, On Surface Var text, 16px side padding
- Active: Primary Container bg (#D2E3FC), On Primary Container text,
bold icon, rounded 28px pill shape (full bleed)
- Hover: Surface 2 bg, On Surface text
- Icon: 20px, left-aligned
- Label: body-md 500 weight, 12px left of icon
Section divider:
Between main items and Logs: 1px divider + "Developer" label (label-sm, muted)
Items (in order):
🏠 Dashboard → /
🔍 Scans → /scans
✈ Airports → /airports
── (divider)
📋 Logs → /logs
Top app bar (desktop)
Height: 64px
Background: Surface (white)
Border-bottom: 1px solid Outline
Shadow: Level 1 on scroll
Left: (sidebar occupies)
Center: Page title (headline-sm, On Surface) — OR breadcrumb
Right: + New Scan button (filled, primary)
Bottom navigation (mobile < 600px)
Height: 80px (includes safe area)
Background: Surface
Border-top: 1px solid Outline
Shadow: Level 2 (upward)
4 items: Dashboard | Scans | Airports | Logs
Each: icon (24px) + label (label-md) stacked
Active: Primary colour icon + Primary Container indicator pill behind icon (56×32px)
Inactive: On Surface Var colour
7.2 Buttons
Filled (primary action — one per page)
Background: Primary (#1A73E8)
Text: On Primary (#FFFFFF)
Height: 40px
Padding: 0 24px
Border-radius: 20px (fully rounded — MD3 style)
Font: label-lg (14px/500)
Shadow: Level 1
Hover: darken 8% + Level 2 shadow
Pressed: darken 12%, Level 1
Disabled: 38% opacity
Icons: optional leading icon at 18px
Outlined (secondary action)
Border: 1px solid Outline (#DADCE0)
Text: Primary (#1A73E8)
Background: transparent
Same sizing/radius as filled
Hover: Primary Container bg (#D2E3FC)
Text (tertiary / destructive link)
Text: Primary or Error
No border, no background
Hover: surface tint
Padding: 0 12px
Icon button
Size: 40×40px
Shape: circle
Background: transparent → Surface 2 on hover
Icon: 20px, On Surface Var → On Surface on hover
7.3 Status Chip / Badge
Height: 24px
Padding: 0 10px
Border-radius: 12px (pill)
Font: label-md (12px/500)
Border: 1px solid (matching border column from §3)
┌──────────────────────┐
│ ● completed │ green bg, green text, green dot
│ ↻ running │ blue bg, blue text, spinning dot (CSS animation)
│ ⧖ pending │ amber bg, amber text, static dot
│ ✕ failed │ red bg, red text, X icon
└──────────────────────┘
Implementation: The colored dot/icon is a 6px circle or SVG icon on the left at 8px margin-right.
7.4 Stat Card
Used on Dashboard (5 cards) and Scan Details (3 cards).
┌────────────────────────┐
│ ┌──┐ │
│ │ 🔍│ Total Scans │ ← icon in tinted circle (40px), label (body-sm, muted)
│ └──┘ │
│ │
│ 54 │ ← number (display-sm or headline-md)
│ │
│ +3 today │ ← optional trend (body-sm, secondary/error)
└────────────────────────┘
Padding: 24px
Border-radius: 16px (MD3 large shape)
Background: Surface (white)
Shadow: Level 1
Width: fluid (grid cell)
Icon circle: 40×40px, border-radius 20px
Total: #E8F0FE bg, Primary icon
Pending: #FEF7E0 bg, Tertiary icon
Running: #E8F0FE bg, Primary icon (animated pulse)
Completed: #E6F4EA bg, Secondary icon
Failed: #FDECEA bg, Error icon
7.5 List / Scan Card
Replaces the current flat <Link> row in the dashboard.
┌──────────────────────────────────────────────────────────┐
│ BDS → FMM, DUS ● completed 25.2.2026 │
│ 26 Feb 2026 – 27 May 2026 · 1 adult · Economy │
│ │
│ 182 routes · 50 flights found → │
└──────────────────────────────────────────────────────────┘
Padding: 16px 20px
Border-radius: 12px (on hover, or always)
Background: Surface
Hover: Surface 2 bg + Level 2 shadow (lift effect)
Divider: 1px inside, OR 8px gap between cards (prefer cards)
Cursor: pointer
Route arrow (→): On Surface Var, shifts right on hover (transform)
Status chip: right-aligned in first row, always visible
Date metadata: body-sm, On Surface Var
Stats line: body-sm, On Surface Var, shown only when > 0
7.6 Text Input & Form Field
MD3 "Outlined" input variant.
Height: 56px
Border: 1px solid Outline
Border-radius: 4px (corners) — MD3 uses 4px for inputs (not pills)
Background: transparent (on Surface bg it appears inset)
Padding: 16px
Labels: floating label (HTML standard — use <label> + peer CSS)
- Default: body-md, On Surface Var, positioned at 50% vertical
- Focused / has value: body-sm, Primary colour, floats to top border
- Error: body-sm, Error colour
Focus ring: 2px solid Primary (replaces native outline)
Error state: border → Error, label → Error, helper text → Error
Helper text: body-sm, On Surface Var, 4px below input
Select: same styling, chevron icon replaces browser default
(use appearance-none + background-image SVG chevron)
Number input: hide browser spin buttons via CSS, add custom +/- icon buttons
on right side (16px icon, icon-button style)
7.7 Toggle / Segmented Button
Replaces the current "Search by Country / Search by Airports" two-button row.
MD3 Segmented Button:
┌─────────────────────┬─────────────────────┐
│ 🌍 By Country │ ✈ By Airports │
└─────────────────────┴─────────────────────┘
Height: 40px
Border: 1px solid Outline, border-radius: 20px (outer container)
Inner divider: 1px solid Outline
Active segment:
Background: Secondary Container (#C8E6C9)
Text: On Secondary Container (#00391A)
Checkmark icon: 18px, appears left of label with slide-in
Inactive segment:
Background: transparent
Text: On Surface
Hover: Surface 2
Font: label-lg
7.8 Airport Chip (selected airport tags)
Replaces current blue rounded-full divs.
MD3 Input Chip:
┌──────────────────┐
│ ✈ BDS ✕ │
└──────────────────┘
Height: 32px
Padding: 0 8px 0 12px
Border-radius: 8px
Border: 1px solid Outline
Background: Surface 2
Font: label-lg (14px/500)
Icon left: 16px airport icon, On Surface Var colour
✕ button right: 18px, On Surface Var → Error on hover
On hover: Level 1 shadow, border → Primary
7.9 Data Table
Used in Scan Details (routes) and Airports (search results).
Header row:
Background: Surface 2 (#F1F3F4)
Text: label-sm (11px/500, uppercase, tracking-wider)
Colour: On Surface Var
Padding: 12px 16px
Sortable columns: trailing sort icon (chevron-up / chevron-down, 16px)
Active sort column: Primary colour icon + underline
Body row:
Height: 52px (dense) / 64px (comfortable — use comfortable)
Padding: 0 16px
Border-bottom: 1px solid Outline Var
Hover: Surface 2 bg (transition 150ms)
Cursor: pointer (if clickable)
Expanded sub-row:
Background: #F8FDF9 (very light green tint — differentiates from route rows)
Indented 32px on first column
Sub-table: label-sm headers, body-md cells
Price column: Secondary colour (#0F9D58), 500 weight
Transition: smooth height expand (max-height animation)
Price cell:
Font: body-md 500
Colour: Secondary (#0F9D58) — min price
Colour: On Surface Var — avg/max price
Align: right (numbers should be right-aligned)
IATA code cell:
Font: mono-md, Primary colour
Background: Primary Container chip inline
Pagination controls:
Previous / Next: Outlined buttons (smaller: height 32px)
Page indicator: body-sm, On Surface Var
Positioned: space-between, 16px top padding
7.10 Empty States
Every zero-data state needs an illustration + message + action.
Layout: centered column, 64px top padding
┌──────────────────────────────────────────┐
│ │
│ [SVG illustration 160px] │
│ │
│ No scans yet │ headline-sm
│ │
│ Create your first scan to discover │ body-md, On Surface Var
│ flight routes and prices. │
│ │
│ [+ Create Scan] │ Filled primary button
│ │
└──────────────────────────────────────────┘
Illustrations (inline SVG, use a consistent line style):
- Dashboard empty: airplane on runway
- No results: magnifying glass, nothing found
- Scan failed: broken connection / cloud with X
- No logs: clipboard empty
7.11 Loading States
Skeleton screens (preferred over spinners for content)
Shimmer animation: linear-gradient sliding left→right, 1.5s loop
Colour: #E8EAED → #F1F3F4 → #E8EAED
Dashboard skeleton:
- 5 stat card rectangles (full size, rounded-16)
- 8 list item rows (height 72px, with inner shape blocks)
Scan detail skeleton:
- 3 stat cards
- Table with 5 ghost rows
Pattern: use <div className="animate-pulse bg-surface-2 rounded-{n}" style={{height, width}} />
Progress bar (running scan)
Height: 4px (slim, at very top of progress card)
Background: Primary Container
Fill: Primary (#1A73E8)
Border-radius: 2px
Animation: ease-in-out width transition on each poll
Card around progress:
Subtle pulsing border: 1px solid Primary, animation 1s ease-in-out infinite
Label: "Scanning routes..." with spinning icon
Sub-label: "23 / 182 routes · auto-refreshing"
Inline spinner (button loading state)
16px × 16px, Primary colour (white when inside filled button)
Animation: 600ms linear spin
Replaces button label text; button stays same width
7.12 Toast Notifications
Replaces the current inline error/success banners in the form.
Position: bottom-right (desktop), bottom-center (mobile)
Width: 360px (desktop), calc(100vw - 32px) (mobile)
Border-radius: 12px
Shadow: Level 3
┌────────────────────────────────────────┐
│ ✓ Scan created successfully! ID: 54 │ [✕]
└────────────────────────────────────────┘
Variants:
Success: Secondary bg (#E6F4EA), Secondary icon, On Surface text
Error: Error Container bg (#FDECEA), Error icon, On Surface text
Info: Primary Container bg (#D2E3FC), Info icon, On Surface text
Warning: Tertiary Container bg (#FFF0C0), Warning icon, On Surface text
Duration: 4s auto-dismiss (success), 8s (error), manual dismiss button always
Animation: slide up from bottom + fade in (300ms), fade out (200ms)
7.13 Progress Indicator (Scan Running)
Replaces the current progress bar card.
┌────────────────────────────────────────────────────────┐
│ ↻ Scanning in progress... │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 38% │
│ 23 of 60 routes · Auto-refreshing every 3s │
└────────────────────────────────────────────────────────┘
Card: Surface, Level 1 shadow, border-radius 16px
Border: 1px animated Primary (pulse)
Progress bar: slim (4px), Primary fill, Surface 2 track
Percentage: body-sm, right-aligned
Icon: spinning ↻ in Primary colour, 20px
8. Page Specifications
8.1 Dashboard
Layout: sidebar + main content area with max-w-5xl
Header:
Row: "Dashboard" (headline-md) + "+ New Scan" (filled button, right)
Stats row: 5 columns on desktop, 2+3 on tablet, 1 col on mobile
┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ 🔍 Total │ ⧖ Pending │ ↻ Running │ ✓ Completed │ ✕ Failed │
│ 54 │ 0 │ 0 │ 7 │ 3 │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
Recent Scans section:
- Section title "Recent Scans" + "View all →" link (text button, right)
- Card list (not a table) — each item is a Scan Card (§7.5)
- 10 items max, then "View all" pagination
Empty state: when no scans exist, show centered empty state illustration (§7.10)
8.2 Create Scan (Scans page)
Layout: narrow centered form, max-w-2xl, on Surface background with Surface card
Page title: "New Scan" (headline-md)
Form structure (vertical stepper feel — not a real wizard, just visual grouping):
Section 1: Origin
┌───────────────────────────────────┐
│ Origin Airport │
│ [BDS ▼ Brindisi Airport ✕] │ ← AirportSearch as filled input chip
└───────────────────────────────────┘
Section 2: Destination
┌─── Segmented Button ─────────────┐
│ 🌍 By Country | ✈ By Airports │
└───────────────────────────────────┘
[if country mode]
┌───────────────────────────────────┐
│ Country Code │
│ DE ▼ (with dropdown of common) │
└───────────────────────────────────┘
[if airports mode]
[AirportSearch] → produces chips below
[BDS ✕] [FMM ✕] [DUS ✕]
Section 3: Search Parameters
┌───────────────┬───────────────────┐
│ Window │ Seat Class │
│ [3 months +-] │ [Economy ▼] │
└───────────────┴───────────────────┘
┌───────────────────────────────────┐
│ Adults [1 +-] │
└───────────────────────────────────┘
Actions (bottom, right-aligned):
[Cancel — outlined] [Create Scan — filled]
Validation feedback: red outlined input + error helper text below field (not a banner)
8.3 Scan Details
Breadcrumb: ← Dashboard / Scan #54
Header card:
┌────────────────────────────────────────────────────────────────┐
│ BDS → FMM, DUS ● completed │
│ 26 Feb 2026 – 27 May 2026 · 1 adult · Economy │
│ │
│ Created: 25 Feb 2026, 20:51 │
└────────────────────────────────────────────────────────────────┘
Stats row (3 cards):
┌────────────┬──────────────┬──────────────┐
│ Total │ Routes │ Total │
│ Routes │ Scanned │ Flights │
│ 182 │ 182 │ 50 │
└────────────┴──────────────┴──────────────┘
Progress card (only when running/pending): see §7.13
Routes table:
- Sortable columns: Destination, Flights, Min Price (default sort: Min Price ASC)
- Each route row: expandable → sub-table of individual flights
- Expand icon: chevron-right → chevron-down (16px, smooth rotate animation)
- IATA code in chip style (inline, Primary Container)
- Min price highlighted in Secondary green
- Sub-table background: very light green tint
Mobile table: horizontal scroll, sticky first column (Destination)
8.4 Airports
Page title: "Airports" + helper text "Search the airport database"
Search bar: full-width, 56px tall, with leading search icon (16px, On Surface Var)
Results:
- List card (not table on mobile, table on desktop)
- Desktop table: IATA | Airport Name | City | Country | [Copy] button
- IATA cell: mono-md, Primary colour, fixed 48px wide
- Copy button → icon button (copy icon), shows "Copied!" tooltip 2s
Empty state on load: show popular airports or a "Start typing to search" message with search icon illustration
8.5 Logs
Page title: "System Logs" + auto-refresh toggle (icon button, top right)
Filters row (horizontal):
[All Levels ▼] [Search messages... 🔍] [Clear ✕]
Log list:
- Monospaced font for message text (mono-md)
- Level badge: left-aligned, fixed 72px wide (fills the badge slot)
- Timestamp: body-sm, right-aligned, fixed width
- Module/Function/Line: body-sm, On Surface Var, inline separated by
· - Log message: body-md, mono-md font, wraps on overflow
┌─────────────────────────────────────────────────────────────────────┐
│ INFO Flight Radar API v2.0 startup complete 22:57:33 │
│ api_server · lifespan · line 310 │
├─────────────────────────────────────────────────────────────────────┤
│ ERROR Connection timeout on BDS→JFK 22:57:31 │
│ searcher_v3 · search_route · line 142 │
└─────────────────────────────────────────────────────────────────────┘
- ERROR rows: very subtle red-tinted row background
- WARNING rows: very subtle amber-tinted row background
9. Motion & Animation
All transitions respect prefers-reduced-motion.
| Interaction | Duration | Easing | Notes |
|---|---|---|---|
| Page transition | 200ms | ease-out | Fade + 8px slide up |
| Card hover lift | 150ms | ease-out | Shadow level 1→2, translateY(-2px) |
| Button press | 100ms | ease-in | Scale 0.98 |
| Expand route row | 250ms | ease-in-out | max-height 0→auto |
| Toast enter | 300ms | cubic-bezier(0.34,1.56,0.64,1) | Spring slide up |
| Toast exit | 200ms | ease-in | Fade + slide down |
| Spinner | 600ms | linear | infinite rotate |
| Progress bar | 300ms | ease-in-out | width transition |
| Skeleton shimmer | 1500ms | ease-in-out | background-position loop |
| Status dot pulse | 1000ms | ease-in-out | opacity 1→0.4→1 (running only) |
| Nav active pill | 200ms | ease-out | width/position transition |
/* Reduced motion override */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
10. Icons
Use Lucide React — lightweight, consistent, MIT licensed, tree-shakeable.
npm install lucide-react
Icon catalogue
| Context | Icon | Lucide name |
|---|---|---|
| Dashboard nav | LayoutDashboard |
LayoutDashboard |
| Scans nav | ScanSearch |
ScanSearch |
| Airports nav | MapPin |
MapPin |
| Logs nav | ScrollText |
ScrollText |
| New scan | Plus |
Plus |
| Search | Search |
Search |
| Sort asc | ChevronUp |
ChevronUp |
| Sort desc | ChevronDown |
ChevronDown |
| Expand row | ChevronRight |
ChevronRight |
| Copy | Copy |
Copy |
| Close / remove | X |
X |
| Status: completed | CheckCircle2 |
CheckCircle2 |
| Status: running | Loader2 (animated) |
Loader2 |
| Status: pending | Clock |
Clock |
| Status: failed | XCircle |
XCircle |
| Price (min) | TrendingDown |
TrendingDown |
| Flight / origin | PlaneTakeoff |
PlaneTakeoff |
| Route arrow | ArrowRight |
ArrowRight |
| Back button | ArrowLeft |
ArrowLeft |
| Auto-refresh | RefreshCw |
RefreshCw |
| Adults | Users |
Users |
| Seat class | Armchair |
Armchair |
| Calendar | Calendar |
Calendar |
| Error | AlertCircle |
AlertCircle |
| Warning | AlertTriangle |
AlertTriangle |
| Info | Info |
Info |
Usage pattern:
import { CheckCircle2 } from 'lucide-react';
<CheckCircle2 size={20} className="text-secondary" aria-hidden="true" />
Always add aria-hidden="true" for decorative icons. Use aria-label for icon-only buttons.
11. Implementation Checklist
Dependencies to add
npm install lucide-react
# Optional but recommended:
npm install clsx # conditional class merging
npm install tailwind-merge # safe Tailwind class merging
Tailwind v4 custom tokens (add to index.css)
@import "tailwindcss";
@theme {
/* Colors */
--color-primary: #1A73E8;
--color-primary-container: #D2E3FC;
--color-on-primary: #FFFFFF;
--color-on-primary-container: #003E8C;
--color-secondary: #0F9D58;
--color-secondary-container: #C8E6C9;
--color-on-secondary: #FFFFFF;
--color-on-secondary-container: #00391A;
--color-tertiary: #F4B400;
--color-tertiary-container: #FFF0C0;
--color-error: #D93025;
--color-error-container: #FDECEA;
--color-surface: #FFFFFF;
--color-surface-1: #FFFFFF;
--color-surface-2: #F1F3F4;
--color-surface-variant: #E8EAED;
--color-on-surface: #202124;
--color-on-surface-variant: #5F6368;
--color-outline: #DADCE0;
--color-outline-variant: #F1F3F4;
--color-bg: #F8F9FA;
/* Border radius */
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-full: 9999px;
/* Shadows */
--shadow-1: 0 1px 2px rgba(0,0,0,.08);
--shadow-2: 0 2px 6px rgba(0,0,0,.10);
--shadow-3: 0 4px 12px rgba(0,0,0,.12);
/* Typography */
--font-sans: 'Google Sans', 'Roboto', system-ui, sans-serif;
--font-mono: 'Roboto Mono', 'JetBrains Mono', monospace;
}
Files to create / update (implementation order)
index.css— design tokens (above)index.html— add Google Fonts link for Google Sans + Roboto Monosrc/components/Layout.tsx— sidebar + bottom nav + top barsrc/components/StatusChip.tsx— new reusable status badgesrc/components/StatCard.tsx— new reusable stat cardsrc/components/Button.tsx— Button component with variantssrc/components/Input.tsx— outlined input with floating labelsrc/components/Toast.tsx— update existing to new designsrc/components/EmptyState.tsx— new componentsrc/components/SkeletonCard.tsx— loading skeletonsrc/pages/Dashboard.tsx— use new componentssrc/pages/Scans.tsx— stepper form layoutsrc/pages/ScanDetails.tsx— new table + expand animationsrc/pages/Airports.tsx— table + better empty statesrc/pages/Logs.tsx— monospaced log rows + level colours
Accessibility requirements
- All interactive elements: minimum 44×44px touch target
- Focus rings: 2px solid Primary, 2px offset (visible on all backgrounds)
- Color is never the only differentiator (icons + text confirm status)
aria-live="polite"on dynamic regions (scan progress, search results)role="status"on loading indicators- Keyboard navigation: Tab order matches visual order; Escape closes modals/dropdowns
- Screen reader labels on all icon-only buttons
- Table headers with
scope="col"and sortablearia-sortattributes
This design system is a living document. Update it when adding new components or changing tokens. The canonical implementation lives in frontend/src/.