Monzo
MIT
Dark, modern fintech design system with navy foundations, coral accents, and fluid typography—built for fast-moving financial products and developer teams
Colour (45)
color.cardbgrgba(9, 23, 35, 0.3)
color.badgebgrgb(242, 248, 243)
color.modalbgvar(--color-white)
color.badgetextrgba(9, 23, 35, 0.6)
color.colorlinkrgb(0, 0, 238)
color.colorblackrgb(0, 0, 0)
color.colorwhitergb(255, 255, 255)
color.cardbglightrgb(242, 248, 243)
color.colorshadowrgba(0, 0, 0, 0.16)
color.tabactivebgrgb(59, 76, 84)
color.colorgreen50rgb(242, 248, 243)
color.colornavy700rgb(59, 76, 84)
color.colornavy800rgb(17, 34, 49)
color.colornavy900rgb(9, 23, 35)
color.filldisabledrgb(194, 200, 208)
color.brandsurface1rgb(17, 34, 49)
color.brandsurface2rgb(255, 79, 64)
color.colorcoral500rgb(255, 79, 64)
color.colorfocusbluergb(0, 164, 219)
color.colornavy70010rgba(53, 78, 85, 0.1)
color.colornavy90030rgba(9, 23, 35, 0.3)
color.colornavy90060rgba(9, 23, 35, 0.6)
color.colortealhoverrgb(46, 175, 192)
color.colortextmutedrgba(9, 23, 35, 0.6)
color.brandprimaryctargb(9, 23, 35)
color.buttonbgprimaryvar(--brand-primary-cta)
color.colortextinversergb(255, 255, 255)
color.colortextprimaryrgb(0, 0, 0)
color.brandsecondaryctargba(53, 78, 85, 0.1)
color.buttonbgsecondaryvar(--brand-secondary-cta)
color.buttonbgprimaryondarkvar(--color-white)
color.semanticbackgroundprimaryrgb(255, 255, 255)
color.semanticactionoutlinefocusrgb(0, 164, 219)
color.semanticbackgroundsecondaryrgb(242, 248, 243)
color.motiondialogslideuptotop2bglz@keyframes Dialog_slide-up-to-top__2bglz {
0% { transform: translateY(0px)
color.motionmarkerprogressfillrsyf1@keyframes Marker_progress-fill__rsyF1 {
0% { inline-size: 0px
color.semanticactionfillprimaryhoverrgb(30, 50, 65)
color.semanticactionfillprimaryactivergb(9, 23, 35)
color.semanticactionfillsecondaryhoverrgba(53, 78, 85, 0.18)
color.semanticactionfillsecondaryactivergba(53, 78, 85, 0.25)
color.semanticinverseactionfillprimaryhoverrgba(240, 240, 240, 1)
color.semanticinverseactionfillprimaryactivergba(220, 220, 220, 1)
color.semanticinverseactionfillsecondaryhoverrgba(255, 255, 255, 0.12)
color.semanticinversebackgroundoverlaylighterrgba(255, 255, 255, 0.08)
color.semanticinverseactionfillsecondaryactivergba(255, 255, 255, 0.20)
Spacing (14)
spacing.spacing3xsmall[TBD - extract manually]
spacing.badgepadding4px 12px
spacing.tabactivepadding8px 16px
spacing.spacexs16px
spacing.spacesm24px
spacing.modalpadding24px 0px 16px
spacing.spacemd48px
spacing.spacelg60px
spacing.spacexl64px
spacing.space2xl128px
spacing.bpmobile480px
spacing.bptablet768px
spacing.bpdesktop1024px
spacing.bpwide1440px
Radius (10)
cardradiusvar(--radius-md)
badgeradiusvar(--radius-sm)
buttonradiusvar(--radius-full)
tabactiveradiusvar(--radius-full)
radiussm4px
radiusmd24px
modalradius32px
radiuslg50%
radiusfull64px
buttonborderradiuscomputed500px
Shadow (4)
effect.shadownonenone
effect.modalshadowrgba(0, 0, 0, 0.16) 0px 8px 32px 0px
effect.reachdialog1
effect.shadowmodalrgba(0,0,0,0.16) 0px 8px 32px 0px
# layout.md — Monzo Design System
---
## 0. Quick Reference
> Inject this block into CLAUDE.md or .cursorrules for immediate AI context.
**Stack:** Next.js · CSS custom properties · Custom font system (MonzoSansDisplay + MonzoSansText)
**Token source:** extracted-css-vars (high confidence, 24 CSS custom properties + computed styles)
**How to apply:** Use as `var(--token-name)` in CSS, `style={{ prop: 'var(--token-name)' }}` in JSX, or `bg-[var(--token-name)]` in Tailwind.
```css
:root {
/* Colours */
--brand-primary-cta: rgb(9, 23, 35); /* Primary CTA / dark navy bg */
--brand-secondary-cta: rgba(53, 78, 85, 0.1); /* Ghost/secondary button bg */
--brand-surface-1: rgb(17, 34, 49); /* Dark hero surface */
--brand-surface-2: rgb(255, 79, 64); /* Coral accent / app download bg */
/* Typography */
--font-stack-title: "MonzoSansDisplay", sans-serif;
--font-stack-body: "MonzoSansText", sans-serif;
--default-line-height: 1.4;
/* Fluid type scale */
--text-billboard: var(--step-6); /* clamp(2.986rem, 2.6276rem + 1.7918vw, 3.8147rem) */
--text-heading-1: var(--step-5); /* clamp(2.4883rem, 2.2447rem + 1.2182vw, 3.0518rem) */
--text-heading-2: var(--step-4); /* clamp(2.0736rem, 1.9145rem + 0.7953vw, 2.4414rem) */
--text-heading-3: var(--step-3); /* clamp(1.728rem, 1.6306rem + 0.4868vw, 1.9531rem) */
/* Spacing */
--space-xs: 16px; --space-sm: 24px;
--space-md: 48px; --space-lg: 60px;
--space-xl: 64px; --space-2xl: 128px;
/* Radius — pill buttons are the brand standard */
--radius-sm: 4px; /* Tags, badges */
--radius-md: 24px; /* Cards, feature tiles */
--radius-full: 64px; /* Pill buttons — PRIMARY SHAPE */
/* Motion */
--duration-fast: 0.2s; --duration-base: 0.3s; --ease-default: ease;
}
```
```tsx
// Primary pill button — correct token usage
<button
className="btn-primary"
style={{
fontFamily: 'var(--font-stack-body)',
fontSize: '16px', fontWeight: 600,
backgroundColor: 'rgb(255,255,255)',
color: 'var(--brand-primary-cta)',
borderRadius: 'var(--radius-full)',
padding: '12px 24px',
transition: 'background-color var(--duration-fast) var(--ease-default)',
}}
>
Get started
</button>
```
**NEVER rules:**
- NEVER use `border-radius < 64px` on primary CTA buttons — Monzo uses pill shape exclusively
- NEVER substitute Inter, Roboto, or Arial — use `MonzoSansDisplay` (headings) and `MonzoSansText` (body/UI)
- NEVER hardcode hex values — always reference the CSS custom properties above
- NEVER use warm off-white backgrounds — dark navy (`--brand-surface-1`) or white are the surfaces
- NEVER construct Tailwind class names dynamically (e.g. `bg-${color}`) — class purging will strip them
- NEVER omit hover/active/focus states — Monzo components have distinct semantic tokens for all three
- NEVER use `border-radius: 8px` on cards — cards use `--radius-md: 24px`
**Full design system → see layout.md**
---
## 1. Design Direction & Philosophy
### Character & Mood
Monzo's visual language is **confident, modern, and grounded in clarity**. It communicates trustworthiness without feeling corporate — the brand balances a dark, authoritative navy with a high-energy coral accent. The overall feeling is: *a serious bank that doesn't take itself too seriously.*
### Aesthetic Intent
- **Dark-first hero sections** using `--brand-surface-1` (rgb(17, 34, 49)) as the dominant background in marquee contexts
- **High contrast CTA hierarchy**: coral (`--brand-surface-2`, rgb(255, 79, 64)) for the app download / brand moment; near-black navy (`--brand-primary-cta`) as the default button fill on light backgrounds
- **Pill-shaped buttons** are the non-negotiable brand signature — `border-radius: 64px` (or `500px` on buttons, matching the computed style). This is not a suggestion; it defines the brand.
- **Fluid typography** via CSS clamp — type scales smoothly across viewports without hard breakpoint jumps
- **Generous whitespace** — `--space-2xl: 128px` section gaps; sections breathe
### What This Design Explicitly Rejects
- **No sharp rectangles on interactive elements** — corners are either pill-shaped (buttons) or moderately rounded (cards at 24px)
- **No default system fonts** — MonzoSansDisplay and MonzoSansText are mandatory; no Inter, no Roboto, no Arial
- **No garish gradients** — surfaces are flat blocks of colour; depth comes from layering, not gradients
- **No warm whites or cream** — the palette is cool: navy darks, clean white, teal-adjacent greens (badge surfaces), coral accent
- **No decorative borders** — UI separation is handled by background colour contrast and spacing, not dividing lines
- **No animation for its own sake** — motion is purposeful (dialog slides, loading spinners, card carousels) and fast (0.2–0.3s)
### Two Font Families, Clear Division of Responsibility
- `MonzoSansDisplay` → headings, marketing copy, billboard text (weights 400–800 + italic variants)
- `MonzoSansText` → all body copy, UI labels, buttons, form elements (weights 400–700 + italic variants)
- Legacy `Maison Neue Web` is present in the font declarations — treat as deprecated; do not introduce in new components
---
## 2. Colour System
### Tier 1 — Primitives (raw values)
```css
/* ── Primitive Palette ── */
:root {
--color-navy-900: rgb(9, 23, 35); /* Darkest navy — CTA fill, text on light */
--color-navy-800: rgb(17, 34, 49); /* Dark navy — hero/section surface */
--color-navy-700: rgb(59, 76, 84); /* Mid slate — active tab fill */
--color-navy-700-10: rgba(53, 78, 85, 0.1); /* Ghost slate — secondary CTA bg */
--color-navy-900-30: rgba(9, 23, 35, 0.3); /* Translucent navy — card overlay */
--color-navy-900-60: rgba(9, 23, 35, 0.6); /* Badge text */
--color-coral-500: rgb(255, 79, 64); /* Coral — app download surface */
--color-white: rgb(255, 255, 255); /* Pure white — button fill (on dark bg), modal bg */
--color-black: rgb(0, 0, 0); /* Pure black — body text */
--color-green-50: rgb(242, 248, 243); /* Pale green — badge bg, card surface variant */
--color-focus-blue: rgb(0, 164, 219); /* Focus ring — locale/select elements */
--color-link: rgb(0, 0, 238); /* Browser-default link (legacy) */
--color-teal-hover: rgb(46, 175, 192); /* Link hover on light surfaces */
--color-shadow: rgba(0, 0, 0, 0.16); /* Modal drop shadow */
}
```
### Tier 2 — Semantic Aliases (intent-named)
```css
/* ── Semantic Layer — preserving original CSS var names where they exist ── */
:root {
/* Surfaces */
--brand-surface-1: rgb(17, 34, 49); /* extracted: high confidence — dark hero bg */
--brand-surface-2: rgb(255, 79, 64); /* extracted: low confidence mapping — coral app section bg */
/* Actions */
--brand-primary-cta: rgb(9, 23, 35); /* extracted: high confidence — primary button bg */
--brand-secondary-cta: rgba(53, 78, 85, 0.1); /* extracted: medium confidence — ghost button bg */
/* Semantic action states — referenced by component CSS, values inferred from usage */
--semantic-action-fill-primary-hover: rgb(30, 50, 65); /* reconstructed: moderate — darkened navy on hover */
--semantic-action-fill-primary-active: rgb(9, 23, 35); /* reconstructed: moderate — same as default on active (0s transition) */
--semantic-action-fill-secondary-hover: rgba(53, 78, 85, 0.18); /* reconstructed: moderate — ghost bg deepened */
--semantic-action-fill-secondary-active: rgba(53, 78, 85, 0.25); /* reconstructed: moderate */
/* Inverse action states (dark surface context — white buttons) */
--semantic-inverse-action-fill-primary-hover: rgba(240, 240, 240, 1); /* reconstructed: moderate — white dims on hover */
--semantic-inverse-action-fill-primary-active: rgba(220, 220, 220, 1); /* reconstructed: moderate */
--semantic-inverse-action-fill-secondary-hover: rgba(255, 255, 255, 0.12); /* reconstructed: moderate */
--semantic-inverse-action-fill-secondary-active: rgba(255, 255, 255, 0.20); /* reconstructed: moderate */
/* Backgrounds */
--semantic-background-primary: rgb(255, 255, 255); /* reconstructed: moderate — StepItem hover bg */
--semantic-background-secondary: rgb(242, 248, 243); /* reconstructed: moderate — Accordion hover bg */
--semantic-inverse-background-overlay-lighter: rgba(255, 255, 255, 0.08); /* reconstructed: moderate — inverse step item */
/* Focus */
--semantic-action-outline-focus: rgb(0, 164, 219); /* extracted: high — checkbox focus ring */
/* Text */
--color-text-primary: rgb(0, 0, 0); /* extracted: high — body default */
--color-text-muted: rgba(9, 23, 35, 0.6); /* extracted: high — badge text */
--color-text-inverse: rgb(255, 255, 255); /* extracted: high — text on dark surfaces */
/* Checkbox states — referenced directly from CSS rules */
--checkbox-checkmark-disabled: [TBD - extract manually]; /* referenced in CSS, value not computed */
--checkbox-outer-hover: [TBD - extract manually];
--checkbox-outer-active: [TBD - extract manually];
--checkbox-content-disabled: [TBD - extract manually];
--fill-disabled: rgb(194, 200, 208); /* extracted: high — #c2c8d0 from CSS rule */
}
```
### Tier 3 — Component Tokens
```css
/* ── Component-specific tokens ── */
:root {
/* Button */
--button-bg-primary: var(--brand-primary-cta);
--button-bg-primary-on-dark: var(--color-white);
--button-bg-secondary: var(--brand-secondary-cta);
--button-text-primary: var(--color-white);
--button-text-primary-on-dark: var(--brand-primary-cta);
--button-radius: var(--radius-full); /* 64px — pill */
--button-border-radius-computed: 500px; /* extracted from button_primary computed style */
/* Card */
--card-radius: var(--radius-md); /* 24px */
--card-bg: rgba(9, 23, 35, 0.3); /* translucent navy overlay */
--card-bg-light: rgb(242, 248, 243); /* pale green variant */
--card-transition: transform 0.2s cubic-bezier(0, 0, 0.12, 1) 0.2s;
/* Badge */
--badge-bg: rgb(242, 248, 243);
--badge-text: rgba(9, 23, 35, 0.6);
--badge-radius: var(--radius-sm); /* 4px */
--badge-padding: 4px 12px;
/* Modal */
--modal-bg: var(--color-white);
--modal-radius: 32px; /* extracted from modal computed styles — distinct from card */
--modal-shadow: rgba(0, 0, 0, 0.16) 0px 8px 32px 0px;
--modal-padding: 24px 0px 16px;
/* Active Tab (role_tab) */
--tab-active-bg: rgb(59, 76, 84);
--tab-active-text: var(--color-white);
--tab-active-radius: var(--radius-full); /* 64px */
--tab-active-padding: 8px 16px;
}
```
### Colour Usage Table
| Token | Value | Role |
|---|---|---|
| `--brand-primary-cta` | `rgb(9, 23, 35)` | Primary button fill (light bg context) |
| `--brand-secondary-cta` | `rgba(53, 78, 85, 0.1)` | Ghost/secondary button |
| `--brand-surface-1` | `rgb(17, 34, 49)` | Hero / dark section background |
| `--brand-surface-2` | `rgb(255, 79, 64)` | **Coral** — app download CTA section |
| `--color-white` | `rgb(255, 255, 255)` | Button fill on dark bg; modal bg |
| `--color-green-50` | `rgb(242, 248, 243)` | Badge background; light card surface |
| `--color-focus-blue` | `rgb(0, 164, 219)` | Focus ring outline |
| `--color-shadow` | `rgba(0,0,0,0.16)` | Modal elevation shadow |
---
## 3. Typography System
### Font Families
```css
:root {
--font-stack-title: "MonzoSansDisplay", sans-serif; /* extracted: low (computed confirms usage) */
--font-stack-body: "MonzoSansText", sans-serif; /* extracted: high */
--default-font-stack: var(--font-stack-body);
--default-line-height: 1.4;
}
```
### Fluid Type Scale (Composite)
```css
/* ── Raw step values (fluid, clamp-based) ── */
:root {
--step--2: clamp(0.64rem, 0.718rem + -0.1177vw, 0.6944rem); /* ~10px — rarely used */
--step--1: clamp(0.8rem, 0.8477rem + -0.0721vw, 0.8333rem); /* ~13px */
--step-0: clamp(1rem, 1rem + 0vw, 1rem); /* 16px — body base */
--step-1: clamp(1.2rem, 1.1784rem + 0.1081vw, 1.25rem); /* ~19–20px */
--step-2: clamp(1.44rem, 1.387rem + 0.2649vw, 1.5625rem); /* ~23–25px */
--step-3: clamp(1.728rem, 1.6306rem + 0.4868vw, 1.9531rem); /* ~28–31px */
--step-4: clamp(2.0736rem, 1.9145rem + 0.7953vw, 2.4414rem);/* ~33–39px */
--step-5: clamp(2.4883rem, 2.2447rem + 1.2182vw, 3.0518rem);/* ~40–49px */
--step-6: clamp(2.986rem, 2.6276rem + 1.7918vw, 3.8147rem); /* ~48–61px */
}
/* ── Semantic text roles (aliases to steps) ── */
:root {
--text-billboard: var(--step-6); /* Hero headline — MonzoSansDisplay, weight 800 */
--text-heading-1: var(--step-5); /* Page title — MonzoSansDisplay, weight 800 */
--text-heading-2: var(--step-4); /* Section heading — MonzoSansDisplay, weight 800 */
--text-heading-3: var(--step-3); /* Sub-section heading — MonzoSansDisplay, weight 800 */
--text-heading-4: var(--step-2); /* Card heading — MonzoSansDisplay, weight 800 */
--text-heading-5: var(--step-1); /* Small heading — MonzoSansDisplay */
--text-body-large: var(--step-1); /* Lead paragraph — MonzoSansText */
--text-body: var(--step-0); /* Default body — MonzoSansText, 16px */
--text-body-small: var(--step--1); /* Small text, captions — MonzoSansText */
--text-body-xsmall: var(--step--1); /* Micro text — MonzoSansText */
}
```
### Composite Typography Groups
| Role | Font Family | Size Token | Computed Size | Weight | Line Height |
|---|---|---|---|---|---|
| **Billboard** | MonzoSansDisplay | `--text-billboard` | ~48–61px fluid | 800 | 1.2 (tight) |
| **Heading 1** | MonzoSansDisplay | `--text-heading-1` | ~40–49px fluid | 800 | 1.2 |
| **Heading 2** | MonzoSansDisplay | `--text-heading-2` | 39.0624px computed | 800 | 46.87px (1.2) |
| **Heading 3** | MonzoSansDisplay | `--text-heading-3` | 31.2496px computed | 800 | 37.4995px (1.2) |
| **Body Large** | MonzoSansText | `--text-body-large` | ~19–20px fluid | 400 | 1.4 |
| **Body** | MonzoSansText | `--text-body` | 16px | 400 | 22.4px (1.4) |
| **Body Small** | MonzoSansText | `--text-body-small` | ~13px fluid | 400 | 1.4 |
| **UI / Button** | MonzoSansText | `--text-body` | 16px | 600 | 22.4px |
| **Badge / Label** | MonzoSansText | `--text-body` | 16px | 400 | 17.6px |
### Weight Scale
```css
:root {
--font-weight-regular: 400; /* Body copy, labels, links — extracted: high */
--font-weight-medium: 600; /* UI elements, buttons, sub-headings — extracted: high */
--font-weight-semibold: 700; /* Stat callouts, highlighted figures — extracted: high */
--font-weight-bold: 800; /* All display headings (h1–h4) — extracted: high */
}
```
> **Note on naming:** Monzo's weight tokens are offset by one step from conventional naming — `--font-weight-medium` maps to CSS `font-weight: 600` (semibold in most systems). Use the token names exactly as defined.
### Pairing Rules
- `MonzoSansDisplay` is **always** used for `h1`–`h4` and billboard text
- `MonzoSansText` is **always** used for body, UI, buttons, forms, captions
- `Oldschool` (OldschoolGroteskCompact-ExtraBold, weight 800) — present in declarations; use only in explicitly branded contexts (not general UI)
- Do not mix `MonzoSansDisplay` at small sizes (below `--step-2`) — it is a display face optimised for large settings
---
## 4. Spacing & Layout
### Base Unit & Scale
```css
:root {
/* ── Spacing scale — extracted: high confidence ── */
--space-xs: 16px; /* Component internal padding, small gaps */
--space-sm: 24px; /* Card padding, modal padding, tab margins */
--space-md: 48px; /* Section internal padding */
--space-lg: 60px; /* Section vertical rhythm (moderate) */
--space-xl: 64px; /* Large section gaps */
--space-2xl: 128px; /* Inter-section gaps, hero vertical space — extracted: high */
/* ── Legacy / context spacing (from computed styles) ── */
--spacing-3x-small: [TBD - extract manually]; /* referenced by focus-outline-offset in Button/IconButton */
--size-3x-small: [TBD - extract manually]; /* referenced by Slide focus inset */
}
```
### Border Radius Scale
```css
:root {
--radius-sm: 4px; /* Tags, badges — 2 elements — extracted: high */
--radius-md: 24px; /* Cards, feature panels — 13 elements — extracted: high */
--radius-lg: 50%; /* Circular paginator dots — 3 elements — extracted: high */
--radius-full: 64px; /* **PRIMARY BUTTON SHAPE** — pill — extracted: high */
/* Note: button_primary computed style shows 500px — both produce pill; use 64px token */
/* Note: modal radius is 32px — between --radius-md and --radius-full; treat as component-specific */
}
```
### Grid & Breakpoints
Monzo has a dense breakpoint set. Key structural breakpoints:
| Breakpoint | px Value | Likely Purpose |
|---|---|---|
| Mobile S | 394px | iPhone SE / small phones |
| Mobile M | 430px | iPhone 14/15 standard |
| Mobile L | 480px / 481px | Large phones / foldables |
| Phablet | 600px | Tablet portrait entry |
| Tablet | 767px / 768px / 769px | iPad portrait |
| Tablet L | 820px / 821px | iPad Air |
| Tablet XL | 850px / 852px | Large tablets |
| Desktop S | 900px | Small desktop / landscape tablet |
| Desktop M | 1024px | Standard laptop |
| Desktop L | 1040px / 1041px | Wide laptop |
| Desktop XL | 1250px / 1300px | Full desktop |
| Max | 1440px | Wide monitor cap |
| Full | 1567px / 1568px | Ultra-wide / TV |
```css
/* Practical breakpoint references */
:root {
--bp-mobile: 480px;
--bp-tablet: 768px;
--bp-desktop: 1024px;
--bp-wide: 1440px;
}
```
### Layout Conventions
- **Navigation:** `display: flex; flex-direction: row; justify-content: space-between; align-items: center; gap: 16px` — full-width, no containing max-width on the nav bar itself
- **Cards:** `display: block` at component level; parent grid controls column layout
- **Primary buttons:** `display: flex; flex-direction: row; justify-content: center; align-items: center; padding: 12px 24px`
- **Button groups (cls_btn):** `display: inline-flex; flex-direction: row; gap: 16px; margin-top: 32px`
- **Large card sections (cls_card):** `display: flex; flex-direction: column; gap: 128px; padding: 128px 0` — full-bleed, pill-radius container
- **Modals:** `display: flex; flex-direction: column; margin: 0 208px` (desktop — centred with large side margins)
---
## 5. Page Structure & Layout Patterns
> All rows are inferred from the layout digest component inventory and computed styles. No screenshots were available. Rows marked "(inferred)" are reconstructed from structural signals.
### 5.1 Section Map
| # | Section Name | Layout Type | Approx. Height | Key Elements |
|---|---|---|---|---|
| 1 | **Navigation** | flex row, space-between | 64–80px | Logo, nav links (gap:16px), CTA button(s) |
| 2 | **Hero / Billboard** | Full-width block | 500–700px | Billboard text (`--text-billboard`), sub-copy, pill CTA buttons, phone/card illustration (inferred) |
| 3 | **Feature Card Grid** | flex column or CSS grid | Variable | 220 card instances — card radius 24px, translucent navy or green-50 bg |
| 4 | **Phone Feature Carousel** | Horizontal scroll / carousel | 400–600px (inferred) | PhoneCarousel buttons (24px radius), tab pills (64px radius), phone mockup |
| 5 | **Stats / Social Proof** | flex row, 3-col (inferred) | 200–300px (inferred) | h3 stat figures (weight 700), body copy |
| 6 | **Payday / CTA Section** | Full-width dark surface | 400–500px (inferred) | `--brand-surface-1` bg, white pill buttons, coral accent |
| 7 | **App Download** | Full-width coral section | 300–400px (inferred) | `--brand-surface-2` coral bg, app store badges, QR code |
| 8 | **Feature Steps / Accordion** | flex column | Variable (inferred) | StepItem components, Accordion with secondary bg on hover |
| 9 | **Testimonials / Reviews** | Carousel or grid (inferred) | 300px (inferred) | Card components, paginator dots (radius 50%) |
| 10 | **Footer** | flex row, multi-column (inferred) | 300–400px (inferred) | Nav links, legal text (`--text-body-small`), locale button |
### 5.2 Layout Patterns
**Navigation (extracted):**
```
display: flex | flex-direction: row | justify-content: space-between | align-items: center | gap: 16px
```
Full viewport width. Logo left, nav items centre/right, CTA button(s) right. No horizontal padding defined at nav level — set by page container.
**Hero (inferred from billboard + button_primary computed styles):**
- Dark surface (`--brand-surface-1`) or white, full bleed
- Billboard text left-aligned or centred
- CTA button group: `inline-flex`, `gap: 16px`, `margin-top: 32px`
- Pill buttons: primary (white fill on dark bg) + secondary (ghost, `--brand-secondary-cta`)
**Feature Card Grid (inferred from 220 card instances):**
- Cards use `border-radius: 24px`, `background: rgba(9,23,35,0.3)` or `rgb(242,248,243)`
- Card transition: `transform 0.2s cubic-bezier(0,0,0.12,1) 0.2s` — anticipatory easing on hover
- Parent sections use `padding: 128px 0`, `gap: 128px` (cls_card pattern)
**Tabs / Filter Pills (extracted from role_tab):**
- Active tab: `background: rgb(59,76,84)`, `color: white`, `border-radius: 64px`, `padding: 8px 16px`
- Tab group: `display: flex`, `gap: 8px`, `margin-bottom: 16px`
**Modal (extracted):**
- `display: flex; flex-direction: column`
- `border-radius: 32px; padding: 24px 0px 16px`
- `box-shadow: rgba(0,0,0,0.16) 0px 8px 32px 0px`
- Desktop margin: `0 208px` — significantly centred
### 5.3 Visual Hierarchy
- **Primary CTA colour:** `rgb(255,255,255)` (white pill button) on dark sections; `rgb(9,23,35)` navy pill on white sections
- **Coral (`--brand-surface-2`)** appears on the app download section — high visual weight, used sparingly (1 element in census)
- **Dark navy (`--brand-surface-1`)** dominates hero/marquee contexts — 3 elements in census, all large surface areas
- **Billboard text** (weight 800, MonzoSansDisplay) is the strongest visual anchor per section
- **Badge components** (green-50 bg, muted navy text) appear above section headings to label content type — `margin-bottom: 8px`
- **Paginator dots** (radius 50%) appear below carousels — minimal, understated navigation
### 5.4 Content Patterns
**Standard feature section (inferred):**
```
[Badge label — green-50 bg]
[Section heading — MonzoSansDisplay, 800, --text-heading-2]
[Body copy — MonzoSansText, 400, --text-body]
[CTA button group — inline-flex, gap:16px, mt:32px]
[Feature card grid or carousel]
```
**Stat / social proof block (inferred):**
```
[Stat figure — MonzoSansText, 700, --text-heading-3 or larger]
[Stat label — MonzoSansText, 400, --text-body]
Repeat × 3 in flex row
```
**Phone carousel pattern (extracted from component inventory):**
- Tab pills (64px radius) filter feature content
- Phone mockup shows active feature
- Content buttons (24px radius) appear alongside the phone UI
---
## 6. Component Patterns
### 6.1 Button
**Anatomy:** Container pill → (optional icon) + label text
**Variants:** `primary`, `secondary`, `primaryInverse`, `secondaryInverse`
**Token mappings:**
| State | Background | Text | Border | Transition |
|---|---|---|---|---|
| Default (primary) | `--brand-primary-cta` | `--color-white` | none | — |
| Hover (primary) | `--semantic-action-fill-primary-hover` | `--color-white` | — | `background-color var(--duration-fast)` |
| Active (primary) | `--semantic-action-fill-primary-active` | `--color-white` | — | `transition: none` (0s) |
| Focus-visible | — | — | `outline var(--semantic-action-outline-focus)` | — |
| Disabled | `--fill-disabled` (#c2c8d0) | muted | — | cursor: not-allowed |
| Loading | — | — | spinner animation | — |
| Default (inverse) | `--color-white` | `--brand-primary-cta` | 1px solid white | — |
| Hover (inverse) | `--semantic-inverse-action-fill-primary-hover` | `--brand-primary-cta` | — | — |
```tsx
// Button.tsx — production-ready with all states
import { ButtonHTMLAttributes, ReactNode } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'primaryInverse' | 'secondaryInverse';
isLoading?: boolean;
children: ReactNode;
}
export function Button({
variant = 'primary',
isLoading = false,
disabled,
children,
...props
}: ButtonProps) {
const isDisabled = disabled || isLoading;
const styles: Record<string, React.CSSProperties> = {
primary: {
backgroundColor: 'var(--brand-primary-cta)',
color: 'var(--color-white, #fff)',
border: 'none',
},
secondary: {
backgroundColor: 'var(--brand-secondary-cta)',
color: 'var(--brand-primary-cta)',
border: 'none',
},
primaryInverse: {
backgroundColor: 'var(--color-white, #fff)',
color: 'var(--brand-primary-cta)',
border: '1px solid var(--color-white, #fff)',
},
secondaryInverse: {
backgroundColor: 'transparent',
color: 'var(--color-white, #fff)',
border: '1px solid rgba(255,255,255,0.4)',
},
};
return (
<button
{...props}
aria-disabled={isDisabled}
disabled={isDisabled}
style={{
/* Layout */
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: '8px',
/* Shape */
borderRadius: 'var(--radius-full, 64px)',
padding: '12px 24px',
/* Typography */
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
fontWeight: 600,
lineHeight: '22.4px',
textDecoration: 'none',
/* Motion */
transition: 'background-color var(--duration-fast, 0.2s) var(--ease-default, ease)',
/* Cursor */
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.5 : 1,
/* Variant */
...styles[variant],
}}
>
{isLoading ? (
<span
aria-label="Loading"
style={{
display: 'inline-block',
width: '16px',
height: '16px',
border: '2px solid currentColor',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin var(--duration-base, 0.3s) linear infinite',
}}
/>
) : (
children
)}
</button>
);
}
```
---
### 6.2 Card
**Anatomy:** Container (24px radius) → optional image → content area (heading + body + optional CTA)
**Token mappings:**
| State | Background | Radius | Transition |
|---|---|---|---|
| Default | `rgba(9,23,35,0.3)` or `rgb(242,248,243)` | `--radius-md` (24px) | — |
| Hover | transform scale-up | `--radius-md` | `transform 0.2s cubic-bezier(0,0,0.12,1) 0.2s` |
| Focus | outline `--semantic-action-outline-focus` | — | — |
| Active | — (no distinct style extracted) | — | — |
| Disabled | opacity 0.5 (inferred) | — | — |
```tsx
// Card.tsx — feature card with hover lift
interface CardProps {
heading: string;
body: string;
variant?: 'dark' | 'light';
href?: string;
}
export function Card({ heading, body, variant = 'light', href }: CardProps) {
return (
<div
style={{
display: 'block',
borderRadius: 'var(--radius-md, 24px)',
backgroundColor:
variant === 'dark'
? 'rgba(9, 23, 35, 0.3)'
: 'var(--color-green-50, rgb(242, 248, 243))',
padding: 'var(--space-sm, 24px)',
transition: 'transform 0.2s cubic-bezier(0, 0, 0.12, 1) 0.2s',
cursor: href ? 'pointer' : 'default',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLDivElement).style.transform = 'scale(1.02)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLDivElement).style.transform = 'scale(1)';
}}
>
<h3
style={{
fontFamily: 'var(--font-stack-title)',
fontSize: 'var(--text-heading-3)',
fontWeight: 'var(--font-weight-bold, 800)',
lineHeight: 1.2,
color: variant === 'dark' ? 'var(--color-white)' : 'var(--color-black)',
margin: '0 0 var(--space-xs, 16px)',
}}
>
{heading}
</h3>
<p
style={{
fontFamily: 'var(--font-stack-body)',
fontSize: 'var(--text-body)',
fontWeight: 'var(--font-weight-regular, 400)',
lineHeight: 'var(--default-line-height, 1.4)',
color: variant === 'dark' ? 'rgba(255,255,255,0.8)' : 'var(--color-black)',
margin: 0,
}}
>
{body}
</p>
</div>
);
}
```
---
### 6.3 Badge
**Anatomy:** Inline block → label text (no icon in extracted data)
```tsx
export function Badge({ children }: { children: string }) {
return (
<span
style={{
display: 'block',
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
fontWeight: 'var(--font-weight-regular, 400)',
lineHeight: '22.4px',
color: 'rgba(9, 23, 35, 0.6)',
backgroundColor: 'var(--color-green-50, rgb(242, 248, 243))',
borderRadius: 'var(--radius-sm, 4px)',
padding: 'var(--badge-padding, 4px 12px)',
marginBottom: '8px',
width: 'fit-content',
}}
>
{children}
</span>
);
}
```
---
### 6.4 Navigation
**Anatomy:** `<nav>` → flex row [Logo] [Nav links] [CTA buttons]
**Token mappings:**
| State | Property | Value |
|---|---|---|
| Default | display, gap | flex row, gap: 16px |
| Default | justify / align | space-between / center |
| Link default | text-decoration | none |
| Link hover | text-decoration | underline |
| Link active | text-decoration | none |
| Focus-visible | outline | 2px solid `--color-focus-blue` |
```tsx
export function Navigation() {
return (
<nav
role="navigation"
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
padding: '0 var(--space-sm, 24px)',
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
fontWeight: 400,
lineHeight: '22.4px',
}}
>
<a href="/" aria-label="Monzo home">
{/* Logo SVG */}
</a>
<ul style={{ display: 'flex', gap: '16px', listStyle: 'none', margin: 0, padding: 0 }}>
{['Personal', 'Business', 'About'].map((item) => (
<li key={item}>
<a
href={`/${item.toLowerCase()}`}
style={{
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
fontWeight: 400,
color: 'inherit',
textDecoration: 'none',
transition: 'color var(--duration-fast) var(--ease-default)',
}}
>
{item}
</a>
</li>
))}
</ul>
<Button variant="primary">Get started</Button>
</nav>
);
}
```
---
### 6.5 Tab / Filter Pill
**Anatomy:** Tab list container → individual tab items (pill-shaped when active)
```tsx
interface TabProps {
tabs: string[];
activeIndex: number;
onChange: (i: number) => void;
}
export function TabBar({ tabs, activeIndex, onChange }: TabProps) {
return (
<div
role="tablist"
style={{
display: 'flex',
flexDirection: 'row',
gap: '8px',
marginBottom: '16px',
flexWrap: 'wrap',
}}
>
{tabs.map((tab, i) => (
<button
key={tab}
role="tab"
aria-selected={i === activeIndex}
onClick={() => onChange(i)}
style={{
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
fontWeight: i === activeIndex ? 600 : 400,
lineHeight: '22.4px',
color: i === activeIndex ? 'rgb(255,255,255)' : 'inherit',
backgroundColor:
i === activeIndex
? 'var(--tab-active-bg, rgb(59,76,84))'
: 'transparent',
borderRadius: 'var(--radius-full, 64px)',
padding: '8px 16px',
border: 'none',
cursor: 'pointer',
display: 'inline-block',
transition:
'background-color var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default)',
}}
>
{tab}
</button>
))}
</div>
);
}
```
---
### 6.6 Modal / Dialog
**Anatomy:** Backdrop → Dialog container (32px radius) → content
**Animations:** `slide-up-from-bottom` enter, `slide-down-to-bottom` exit (mobile); `fade-in`/`fade-out` on backdrop
```tsx
export function Modal({ isOpen, onClose, children }: {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}) {
if (!isOpen) return null;
return (
<div
role="presentation"
style={{
position: 'fixed', inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 1000,
animation: 'fadeIn var(--duration-fast) var(--ease-default)',
}}
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
onClick={(e) => e.stopPropagation()}
style={{
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--color-white, #fff)',
borderRadius: '32px', /* modal-specific, not --radius-md */
padding: 'var(--modal-padding, 24px 0px 16px)',
boxShadow: 'var(--modal-shadow, rgba(0,0,0,0.16) 0px 8px 32px 0px)',
width: 'calc(100% - 416px)', /* mirrors 0 208px margin */
maxWidth: '900px',
fontFamily: 'var(--font-stack-body)',
fontSize: '16px',
lineHeight: '24px',
}}
>
{children}
</div>
</div>
);
}
```
---
## 7. Elevation & Depth
```css
/* ── Shadow tokens ── */
:root {
--shadow-modal: rgba(0, 0, 0, 0.16) 0px 8px 32px 0px;
/* extracted: high — from modal computed style */
/* Usage: modals, dialogs, overlay panels */
--shadow-none: none;
/* Cards, buttons, nav — Monzo uses flat surfaces with colour separation, not shadows */
}
/* ── Z-index scale ── */
:root {
--z-base: 0; /* Normal document flow */
--z-card: 1; /* Hover-lifted cards */
--z-sticky: 100; /* Sticky navigation */
--z-dropdown: 200; /* Nav dropdown panels (inferred) */
--z-modal: 1000; /* Modals and dialogs — --reach-dialog: 1 signals library usage */
--z-toast: 1100; /* Notifications above modals (inferred) */
}
```
### Depth Principles
- **Monzo uses colour contrast for depth, not shadows.** The modal is the only component with a box-shadow.
- Cards achieve separation via `background-color` difference (navy translucent vs. green-50 vs. white), not elevation shadows.
- The `--reach-dialog: 1` CSS variable indicates use of the Reach UI dialog library — respect its z-index contract.
- **Border usage:** almost zero decorative borders. Form focus states use `box-shadow: 0 0 0 2px` as a non-layout focus ring technique.
---
## 8. Motion
### Timing Tokens
```css
:root {
--duration-fast: 0.2s; /* Buttons, cards, colour transitions — extracted: high */
--duration-base: 0.3s; /* Loading spinner, multi-step transitions — extracted: high */
--ease-default: ease; /* Universal easing — extracted: high */
/* Card-specific easing — from computed transition property */
--ease-card-enter: cubic-bezier(0, 0, 0.12, 1); /* Anticipatory enter — decelerates into position */
}
```
### Keyframe Animations (extracted — reference names only in code)
| Animation Name | Usage | Duration |
|---|---|---|
| `spin` (LoadingIndicator) | Loading spinner rotation | continuous |
| `Dialog_slide-up-from-bottom` | Mobile dialog enter | `--duration-base` |
| `Dialog_slide-down-to-bottom` | Mobile dialog exit | `--duration-base` |
| `Dialog_slide-down-from-top` | Desktop dialog enter | `--duration-base` |
| `Dialog_fade-in` | Backdrop fade in | `--duration-fast` |
| `Dialog_fade-out` | Backdrop fade out | `--duration-fast` |
| `FadeImage_slideIn` | Image carousel transition | `--duration-base` |
| `FadeImage_slideOut` | Image carousel transition | `--duration-base` |
| `Navigation_enterFromTop` | Nav panel enter | `--duration-fast` |
| `CardCarouselAnimation_rotate` | Card fan rotation | continuous |
| `Hero_shimmer` | Hero loading shimmer | continuous |
| `Spinner_spinnerRing` | Button loading state | continuous |
### When to Animate
- ✅ **Button state changes** — `background-color` only, `var(--duration-fast)`
- ✅ **Card hover** — `transform` scale/translate, `cubic-bezier(0,0,0.12,1)` with `0.2s` delay
- ✅ **Dialog open/close** — slide + fade, `var(--duration-base)`
- ✅ **Navigation panels** — slide from top/side, `var(--duration-fast)`
- ✅ **Loading states** — continuous spin, no easing (linear)
- ✅ **Image carousels** — opacity fade, `var(--duration-base)`
### When NOT to Animate
- ❌ **Active button press** — `transition: none` at active state (0s duration) — instant feedback
- ❌ **Form inputs** — no animation on focus (outline appears instantly via CSS)
- ❌ **Static text content** — no text entrance animations in base design system
- ❌ **Disabled states** — do not animate into or out of disabled state
- ❌ **Reduce-motion** — wrap all animation declarations in `@media (prefers-reduced-motion: no-preference)`
```css
/* Reduced-motion safety */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
---
## 9. Anti-Patterns & Constraints
**1. Hardcoded colour values in component code**
**Rule:** Never write `rgb(9, 23, 35)` or `#091723` directly in a component.
**Why it fails:** When Monzo's brand palette evolves (or dark/light modes are added), every hardcoded instance requires a manual hunt-and-replace. AI agents pattern-match from examples — if they see one hardcoded colour in a codebase, they'll propagate it everywhere.
**Do instead:** Always use `var(--brand-primary-cta)`, `var(--brand-surface-1)` etc. — the token system is the single source of truth.
**2. Using non-Monzo font families**
**Rule:** Never use `font-family: Inter`, `Arial`, `Roboto`, or any system font in components.
**Why it fails:** AI coding agents default to Inter (the most common design system font) when no font context is given. Rendering MonzoSansText as Inter visually destroys the brand — different x-height, different weight distribution, different kerning.
**Do instead:** Always set `font-family: var(--font-stack-body)` for UI/body and `var(--font-stack-title)` for headings. Never omit the font-family declaration.
**3. Using `border-radius: 8px` on buttons**
**Rule:** Never apply `border-radius: 8px` (or any value below 64px) to primary CTA buttons.
**Why it fails:** AI agents trained on generic design systems default to 8px radius buttons. On Monzo, this breaks the pill-button brand signature — the most visually distinctive element of the UI. The result looks like a different bank entirely.
**Do instead:** Use `border-radius: var(--radius-full, 64px)` for all pill buttons. Cards use `var(--radius-md, 24px)`. The only 8px-or-less radius in the system is `--radius-sm: 4px` for tags and badges.
**4. Applying `--radius-md` (24px) to modals**
**Rule:** Never use `--radius-md: 24px` for modal/dialog components.
**Why it fails:** Monzo's modal border-radius is `32px` — a distinct value not represented in the radius scale tokens. AI agents will reach for the nearest named token (`--radius-md: 24px`) and produce a slightly wrong result that's hard to spot in code review.
**Do instead:** Use the explicit `32px` value for modals (or create a `--radius-modal: 32px` token). Document that the modal radius is not an alias for `--radius-md`.
**5. Constructing Tailwind class names dynamically**
**Rule:** Never write `className={\`bg-[\${color}]\`}` or `className={variant === 'primary' ? 'bg-navy' : 'bg-coral'}` where the class string is built at runtime.
**Why it fails:** Tailwind's build step scans static strings. Dynamically constructed class names are never included in the generated CSS bundle — the styles simply don't exist at runtime. This is a silent failure that only appears in production.
**Do instead:** Use full static class strings (`className="bg-[var(--brand-primary-cta)]"`) or use CSS custom properties directly via `style={{ backgroundColor: 'var(--brand-primary-cta)' }}`.
**6. Omitting interaction states from components**
**Rule:** Never ship a Button, card, or nav item without explicit hover, active, focus-visible, and disabled states.
**Why it fails:** Monzo's CSS rules show distinct semantic tokens for each state (`--semantic-action-fill-primary-hover`, `--semantic-action-fill-primary-active`, `transition: none` on active). AI agents often generate only a hover state and leave active/disabled/focus undefined, creating an inaccessible component that fails WCAG 2.1 AA requirements.
**Do instead:** Use the state table in Section 6.1 as a checklist. Every interactive component needs all five states implemented before it's done.
**7. Using `!important` to override design tokens**
**Rule:** Never use `!important` to override CSS custom property values.
**Why it fails:** CSS custom properties are already overridable by specificity and cascade — `!important` breaks the inheritance chain that allows dark-mode, theming, and component variants to work. Once `!important` is in a codebase, it propagates as each subsequent developer tries to win the specificity war.
**Do instead:** Override tokens at the correct scope: `:root` for global, `.dark-theme` for dark mode (referenced in `.Text_text__cqgPj` CSS rules), component class for component-specific overrides.
**8. Using absolute positioning for card grid layout**
**Rule:** Never use `position: absolute` to arrange cards in a grid.
**Why it fails:** Monzo cards at scale (220 instances) need to reflow correctly across 23 breakpoints. Absolute positioning produces a brittle layout that breaks at any viewport Monzo hasn't manually accounted for, and is incompatible with the flex/grid column system the page uses.
**Do instead:** Use CSS Grid (`grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))`) or flexbox (`display: flex; flex-wrap: wrap; gap: var(--space-sm)`) for card grids.
**9. Mixing `MonzoSansDisplay` and `MonzoSansText` at wrong scales**
**Rule:** Never use `MonzoSansDisplay` for body text (below ~20px / `--step-1`), and never use `MonzoSansText` for display headings (h1–h3).
**Why it fails:** `MonzoSansDisplay` is a display typeface — it has optical characteristics (looser spacing, higher contrast strokes) optimised for large sizes. At small sizes it reads poorly. AI agents, not knowing the purpose distinction, may use whichever font was set last or default to one for everything.
**Do instead:** The rule is simple — font assignment follows element role: heading element → `var(--font-stack-title)`, any body/UI element → `var(--font-stack-body)`.
**10. Placeholder `lorem ipsum` or static mock data in production components**
**Rule:** Never ship components with hardcoded placeholder strings like "Lorem ipsum" or `"Card Title"`.
**Why it fails:** AI agents fill in content when the real data shape isn't specified. Monzo's components are data-driven (feature flags, CMS content, real account data). Placeholder content that ships to production creates misleading UI and accessibility violations (screen readers read the lorem ipsum text aloud).
**Do instead:** Require content props to be passed explicitly. Use TypeScript `string` types — not default string values — to force the caller to provide real content.
---
## Appendix A: Complete Token Reference
Every token extracted from the source. §0 CORE TOKENS is the primary AI signal; this appendix is reference material an AI can cross-check against when a curated role is missing.
```css
/* Colours (47) */
--brand-primary-cta: rgb(9, 23, 35); /* Primary CTA background, dominant on 18 buttons — e.g. "Sign up" /* mined from computed styles */ */
--brand-secondary-cta: rgba(53, 78, 85, 0.1); /* Secondary CTA background, dominant on 4 buttons — e.g. "button" /* mined from computed styles */ */
--brand-surface-1: rgb(17, 34, 49); /* Brand surface, dominant on 3 elements — e.g. "Make payday twice as niceWe co" /* mined from computed styles */ */
--brand-surface-2: rgb(255, 79, 64); /* Brand surface, dominant on 1 element — e.g. "Download the appOpen the App S" /* mined from computed styles */ */
--brand-primary-cta: rgb(9, 23, 35);
--brand-secondary-cta: rgba(53, 78, 85, 0.1);
--brand-surface-1: rgb(17, 34, 49);
--brand-surface-2: rgb(255, 79, 64);
--color-navy-900: rgb(9, 23, 35);
--color-navy-800: rgb(17, 34, 49);
--color-navy-700: rgb(59, 76, 84);
--color-navy-700-10: rgba(53, 78, 85, 0.1);
--color-navy-900-30: rgba(9, 23, 35, 0.3);
--color-navy-900-60: rgba(9, 23, 35, 0.6);
--color-coral-500: rgb(255, 79, 64);
--color-white: rgb(255, 255, 255);
--color-black: rgb(0, 0, 0);
--color-green-50: rgb(242, 248, 243);
--color-focus-blue: rgb(0, 164, 219);
--color-link: rgb(0, 0, 238);
--color-teal-hover: rgb(46, 175, 192);
--color-shadow: rgba(0, 0, 0, 0.16);
--semantic-action-fill-primary-hover: rgb(30, 50, 65);
--semantic-action-fill-primary-active: rgb(9, 23, 35);
--semantic-action-fill-secondary-hover: rgba(53, 78, 85, 0.18);
--semantic-action-fill-secondary-active: rgba(53, 78, 85, 0.25);
--semantic-inverse-action-fill-primary-hover: rgba(240, 240, 240, 1);
--semantic-inverse-action-fill-primary-active: rgba(220, 220, 220, 1);
--semantic-inverse-action-fill-secondary-hover: rgba(255, 255, 255, 0.12);
--semantic-inverse-action-fill-secondary-active: rgba(255, 255, 255, 0.20);
--semantic-background-primary: rgb(255, 255, 255);
--semantic-background-secondary: rgb(242, 248, 243);
--semantic-inverse-background-overlay-lighter: rgba(255, 255, 255, 0.08);
--semantic-action-outline-focus: rgb(0, 164, 219);
--color-text-primary: rgb(0, 0, 0);
--color-text-muted: rgba(9, 23, 35, 0.6);
--color-text-inverse: rgb(255, 255, 255);
--fill-disabled: rgb(194, 200, 208);
--button-bg-primary: var(--brand-primary-cta);
--button-bg-primary-on-dark: var(--color-white);
--button-bg-secondary: var(--brand-secondary-cta);
--card-bg: rgba(9, 23, 35, 0.3);
--card-bg-light: rgb(242, 248, 243);
--badge-bg: rgb(242, 248, 243);
--badge-text: rgba(9, 23, 35, 0.6);
--modal-bg: var(--color-white);
--tab-active-bg: rgb(59, 76, 84);
/* Typography (44) */
--font-stack-title: "MonzoSansDisplay", sans-serif;
--font-stack-body: "MonzoSansText", sans-serif;
--step--2: clamp(0.64rem,0.718rem + -0.1177vw,0.6944rem);
--step--1: clamp(0.8rem,0.8477rem + -0.0721vw,0.8333rem);
--step-0: clamp(1rem,1rem + 0vw,1rem);
--step-1: clamp(1.2rem,1.1784rem + 0.1081vw,1.25rem);
--step-2: clamp(1.44rem,1.387rem + 0.2649vw,1.5625rem);
--step-3: clamp(1.728rem,1.6306rem + 0.4868vw,1.9531rem);
--step-4: clamp(2.0736rem,1.9145rem + 0.7953vw,2.4414rem);
--step-5: clamp(2.4883rem,2.2447rem + 1.2182vw,3.0518rem);
--step-6: clamp(2.986rem,2.6276rem + 1.7918vw,3.8147rem);
--text-billboard: var(--step-6);
--text-heading-1: var(--step-5);
--text-heading-2: var(--step-4);
--text-heading-3: var(--step-3);
--text-heading-4: var(--step-2);
--text-body-large: var(--step-1);
--text-body: var(--step-0);
--text-body-small: var(--step--1);
--default-line-height: 1.4;
--default-font-stack: var(--font-stack-body);
--font-size-xs: 16px; /* 84 elements — e.g. h3 "Great Britain", h3 "Northern Ireland", p "You could get back l" /* mined from computed styles */ */
--font-size-sm: 20px; /* 16 elements — e.g. h3 "Call Status", h3 "Instant notification", h3 "Card freeze" /* mined from computed styles */ */
--font-size-md: 25px; /* 5 elements — e.g. h3 "Products", h3 "Company", h3 "Using Monzo" /* mined from computed styles */ */
--font-size-lg: 31.2496px; /* 14 elements — e.g. h2 "Security you can ban", h3 "Payday, the Monzo wa", h3 "Sort your salary" /* mined from computed styles */ */
--font-size-xl: 39.0624px; /* 9 elements — e.g. h2 "Manage your money to", h2 "Humans on hand, 24/7", h2 "Make payday twice as" /* mined from computed styles */ */
--font-size-2xl: 44px; /* 2 elements — e.g. h2 "Independent service ", h2 "Authorised push paym" /* mined from computed styles */ */
--font-size-3xl: 48.8288px; /* 10 elements — e.g. h2 "Monzo for all your m", h2 "Tell me about...", h2 "Did you know?" /* mined from computed styles */ */
--font-weight-regular: 400; /* 56 elements — e.g. p "Get clear on your sp", p "You could get back l", p "Our signature Hot Co" /* mined from computed styles */ */
--font-weight-medium: 600; /* 45 elements — e.g. h3 "Call Status", h3 "Instant notification", h3 "Card freeze" /* mined from computed styles */ */
--font-weight-semibold: 700; /* 14 elements — e.g. h3 "Over 2 million peopl", h3 "£131", h3 "£8.5 billion" /* mined from computed styles */ */
--font-weight-bold: 800; /* 30 elements — e.g. h2 "Monzo for all your m", h2 "Tell me about...", h2 "Manage your money to" /* mined from computed styles */ */
--line-height-tight: 22.4px; /* 63 elements — e.g. p "As part of a regulat", p "Authorised push paym", p "Monzo Bank Limited i" /* mined from computed styles */ */
--line-height-normal: 28px; /* 10 elements — e.g. p "Get clear on your sp", p "Our signature Hot Co", p "UK residents. Ts&Cs " /* mined from computed styles */ */
--line-height-loose: 37.4995px; /* 14 elements — e.g. h2 "Security you can ban", h3 "Payday, the Monzo wa", h3 "Sort your salary" /* mined from computed styles */ */
--button-text-primary: var(--color-white);
--button-text-primary-on-dark: var(--brand-primary-cta);
--tab-active-text: var(--color-white);
--text-heading-5: var(--step-1);
--text-body-xsmall: var(--step--1);
--font-weight-regular: 400;
--font-weight-medium: 600;
--font-weight-semibold: 700;
--font-weight-bold: 800;
/* Spacing (20) */
--space-xs: 16px; /* 6 elements — e.g. nav .Navigation_navigation__MiBte, nav .Navigation_navigation__MiBte, nav .Navigation_navigation__MiBte /* mined from computed styles */ */
--space-sm: 24px; /* 51 elements — e.g. section .BasicSectionContainer_basicSectionContai, section .BasicSectionContainer_basicSectionContai, section .BasicSectionContainer_basicSectionContai /* mined from computed styles */ */
--space-md: 48px; /* 3 elements — e.g. footer .Footer_footer__BdQyf, footer .Footer_footer__BdQyf, footer .Footer_footer__BdQyf /* mined from computed styles */ */
--space-lg: 60px; /* 3 elements — e.g. div .PhoneCarousel_grid__2rl1W, div .PhoneCarousel_grid__2rl1W, div .PhoneCarousel_grid__2rl1W /* mined from computed styles */ */
--space-xl: 64px; /* 44 elements — e.g. section .BasicSectionContainer_basicSectionContai, section .BasicSectionContainer_basicSectionContai, section .BasicSectionContainer_basicSectionContai /* mined from computed styles */ */
--space-2xl: 128px; /* 8 elements — e.g. div .PaymentCardFan_cardFanSection__yaEoo, div .PaymentCardFan_cardFanSection__yaEoo, div .PaymentCardFan_cardFanSection__yaEoo /* mined from computed styles */ */
--space-xs: 16px;
--space-sm: 24px;
--space-md: 48px;
--space-lg: 60px;
--space-xl: 64px;
--space-2xl: 128px;
--badge-padding: 4px 12px;
--modal-padding: 24px 0px 16px;
--tab-active-padding: 8px 16px;
--spacing-3x-small: [TBD - extract manually];
--bp-mobile: 480px;
--bp-tablet: 768px;
--bp-desktop: 1024px;
--bp-wide: 1440px;
/* Radius (14) */
--radius-sm: 4px; /* 2 elements — e.g. div .Tag_tag__98SrA "Published February 2", div .Tag_tag__98SrA "Published February 2" /* mined from computed styles */ */
--radius-md: 24px; /* 13 elements — e.g. button .PhoneCarousel_phoneContentButton__UN3HE "Call StatusThis feat", button .PhoneCarousel_phoneContentButton__UN3HE "Instant notification", button .PhoneCarousel_phoneContentButton__UN3HE "Card freezeInstantly" /* mined from computed styles */ */
--radius-lg: 50%; /* 3 elements — e.g. button .PaginatorIndicator_paginatorDot__erSTr, button .PaginatorIndicator_paginatorDot__erSTr, button .PaginatorIndicator_paginatorDot__erSTr /* mined from computed styles */ */
--radius-full: 64px; /* 1 element — e.g. div .PaymentCardFan_cardFanSection__yaEoo "It all starts with a" /* mined from computed styles */ */
--radius-sm: 4px;
--radius-md: 24px;
--radius-full: 64px;
--button-radius: var(--radius-full);
--button-border-radius-computed: 500px;
--card-radius: var(--radius-md);
--badge-radius: var(--radius-sm);
--modal-radius: 32px;
--tab-active-radius: var(--radius-full);
--radius-lg: 50%;
/* Effects (4) */
--reach-dialog: 1;
--modal-shadow: rgba(0, 0, 0, 0.16) 0px 8px 32px 0px;
--shadow-modal: rgba(0,0,0,0.16) 0px 8px 32px 0px;
--shadow-none: none;
/* Motion (36) */
----motion-LoadingIndicator_spin__R3reU: @keyframes LoadingIndicator_spin__R3reU {
100% { transform: rotate(1turn); }
}; /* @keyframes LoadingIndicator_spin__R3reU */
----motion-Dialog_slide-up-from-bottom__fTybb: @keyframes Dialog_slide-up-from-bottom__fTybb {
0% { transform: translateY(100%); }
100% { transform: translateY(0px); }
}; /* @keyframes Dialog_slide-up-from-bottom__fTybb */
----motion-Dialog_slide-down-to-bottom__IaASz: @keyframes Dialog_slide-down-to-bottom__IaASz {
0% { transform: translateY(0px); }
100% { transform: translateY(100%); }
}; /* @keyframes Dialog_slide-down-to-bottom__IaASz */
----motion-Dialog_slide-down-from-top__xcLuU: @keyframes Dialog_slide-down-from-top__xcLuU {
0% { transform: translateY(-100%); }
100% { transform: translateY(0px); }
}; /* @keyframes Dialog_slide-down-from-top__xcLuU */
----motion-Dialog_slide-up-to-top__2bglz: @keyframes Dialog_slide-up-to-top__2bglz {
0% { transform: translateY(0px); }
100% { transform: translateY(-100%); }
}; /* @keyframes Dialog_slide-up-to-top__2bglz */
----motion-Dialog_fade-in__H8_ns: @keyframes Dialog_fade-in__H8_ns {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes Dialog_fade-in__H8_ns */
----motion-Dialog_fade-out__ed0nG: @keyframes Dialog_fade-out__ed0nG {
0% { opacity: 1; }
100% { opacity: 0; }
}; /* @keyframes Dialog_fade-out__ed0nG */
----motion-Marker_progress-fill__rsyF1: @keyframes Marker_progress-fill__rsyF1 {
0% { inline-size: 0px; }
100% { inline-size: 100%; }
}; /* @keyframes Marker_progress-fill__rsyF1 */
----motion-CardCarouselAnimation_rotate__Fcd9M: @keyframes CardCarouselAnimation_rotate__Fcd9M {
0%, 6.25% { transform: rotat… <0.4KB elided>; /* @keyframes CardCarouselAnimation_rotate__Fcd9M */
----motion-CardCarouselAnimation_scale__6BAAV: @keyframes CardCarouselAnimation_scale__6BAAV {
0%, 6.25% { transform: scale(… <0.2KB elided>; /* @keyframes CardCarouselAnimation_scale__6BAAV */
----motion-FloatingHeartAnimation_flutter-left__gseaI: @keyframes FloatingHeartAnimation_flutter-left__gseaI {
70% { opacity: 0.5; }… <0.2KB elided>; /* @keyframes FloatingHeartAnimation_flutter-left__gseaI */
----motion-FloatingHeartAnimation_flutter-right___CYMD: @keyframes FloatingHeartAnimation_flutter-right___CYMD {
70% { opacity: 0.5;… <0.2KB elided>; /* @keyframes FloatingHeartAnimation_flutter-right___CYMD */
----motion-FloatingHeartAnimation_flutter-center__G9CKH: @keyframes FloatingHeartAnimation_flutter-center__G9CKH {
70% { opacity: 0.5;… <0.2KB elided>; /* @keyframes FloatingHeartAnimation_flutter-center__G9CKH */
----motion-ShimmeringBankCardAnimation_shimmer__tOAFk: @keyframes ShimmeringBankCardAnimation_shimmer__tOAFk {
30% { inset-inline-start: 150%; }
100% { inset-inline-start: 150%; }
}; /* @keyframes ShimmeringBankCardAnimation_shimmer__tOAFk */
----motion-FadeImage_slideIn__OFQyh: @keyframes FadeImage_slideIn__OFQyh {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes FadeImage_slideIn__OFQyh */
----motion-FadeImage_slideOut__SWgjX: @keyframes FadeImage_slideOut__SWgjX {
0% { opacity: 1; }
100% { opacity: 0; }
}; /* @keyframes FadeImage_slideOut__SWgjX */
----motion-PaymentCardFan_slideIn__9QpON: @keyframes PaymentCardFan_slideIn__9QpON {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes PaymentCardFan_slideIn__9QpON */
----motion-PaymentCardFan_slideOut__yAfoe: @keyframes PaymentCardFan_slideOut__yAfoe {
0% { opacity: 1; }
100% { opacity: 0; }
}; /* @keyframes PaymentCardFan_slideOut__yAfoe */
----motion-Navigation_enterFromTop__ODcdP: @keyframes Navigation_enterFromTop__ODcdP {
0% { opacity: 0; transform: translateY(-100%); }
100% { opacity: 1; transform: translateY(0px); }
}; /* @keyframes Navigation_enterFromTop__ODcdP */
----motion-Navigation_navigationCurtainFadeIn__DelHx: @keyframes Navigation_navigationCurtainFadeIn__DelHx {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes Navigation_navigationCurtainFadeIn__DelHx */
----motion-NavigationTopLevelItems_enterFromRight__rh5Av: @keyframes NavigationTopLevelItems_enterFromRight__rh5Av {
0% { opacity: 0; transform: translateX(100%); }
100% { opacity: 1; transform: translateX(0px); }
}; /* @keyframes NavigationTopLevelItems_enterFromRight__rh5Av */
----motion-NavigationTopLevelItems_exitToRight__B1v3a: @keyframes NavigationTopLevelItems_exitToRight__B1v3a {
0% { opacity: 1; transform: translateX(0px); }
100% { opacity: 0; transform: translateX(100%); }
}; /* @keyframes NavigationTopLevelItems_exitToRight__B1v3a */
----motion-Navigation_enterFromTop___eVNt: @keyframes Navigation_enterFromTop___eVNt {
0% { opacity: 0; transform: translateY(-100%); }
100% { opacity: 1; transform: translateY(0px); }
}; /* @keyframes Navigation_enterFromTop___eVNt */
----motion-Navigation_navigationCurtainFadeIn__m11fe: @keyframes Navigation_navigationCurtainFadeIn__m11fe {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes Navigation_navigationCurtainFadeIn__m11fe */
----motion-NavigationTopLevelItems_enterFromRight__rLqHz: @keyframes NavigationTopLevelItems_enterFromRight__rLqHz {
0% { opacity: 0; transform: translateX(100%); }
100% { opacity: 1; transform: translateX(0px); }
}; /* @keyframes NavigationTopLevelItems_enterFromRight__rLqHz */
----motion-NavigationTopLevelItems_exitToRight__xbwhC: @keyframes NavigationTopLevelItems_exitToRight__xbwhC {
0% { opacity: 1; transform: translateX(0px); }
100% { opacity: 0; transform: translateX(100%); }
}; /* @keyframes NavigationTopLevelItems_exitToRight__xbwhC */
----motion-MobileMenuUS_slideIn__VxxAp: @keyframes MobileMenuUS_slideIn__VxxAp {
100% { transform: translateX(0px); }
0% { transform: translateX(100%); }
}; /* @keyframes MobileMenuUS_slideIn__VxxAp */
----motion-MobileMenuUS_slideOut__L7_vZ: @keyframes MobileMenuUS_slideOut__L7_vZ {
100% { transform: translateX(100%); }
0% { transform: translateX(0px); }
}; /* @keyframes MobileMenuUS_slideOut__L7_vZ */
----motion-Spinner_spinnerRing__b3mNf: @keyframes Spinner_spinnerRing__b3mNf {
0% { transform: rotate(0deg); }
100% { transform: rotate(1turn); }
}; /* @keyframes Spinner_spinnerRing__b3mNf */
----motion-CompanyNameSearchHero_fadeIn__jd2pk: @keyframes CompanyNameSearchHero_fadeIn__jd2pk {
0% { opacity: 0; }
100% { opacity: 1; }
}; /* @keyframes CompanyNameSearchHero_fadeIn__jd2pk */
----motion-Thumbnail_spinning__hZbVz: @keyframes Thumbnail_spinning__hZbVz {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(1turn); }
}; /* @keyframes Thumbnail_spinning__hZbVz */
----motion-IconButton_spin__xwWFV: @keyframes IconButton_spin__xwWFV {
0% { transform: rotate(0deg); }
100% { transform: rotate(1turn); }
}; /* @keyframes IconButton_spin__xwWFV */
----motion-Hero_shimmer__fRYxU: @keyframes Hero_shimmer__fRYxU {
0% { left: -100%; }
50% { left: 150%; }
100% { left: 150%; }
}; /* @keyframes Hero_shimmer__fRYxU */
--duration-fast: 0.2s; /* 30 elements — e.g. button, button, button /* mined from computed styles */ */
--duration-base: 0.3s; /* 4 elements — e.g. span, span, span /* mined from computed styles */ */
--ease-default: ease; /* 248 elements — e.g. button, button, button /* mined from computed styles */ */
```
## Appendix B: Token Source Metadata
| Property | Value |
|---|---|
| **Source site** | monzo.com |
| **Token source** | `extracted-css-vars` |
| **Overall confidence** | High — 24 CSS custom properties extracted directly |
| **Extraction method** | CSS custom property scan + computed style sampling on key elements |
| **Libraries detected** | Bootstrap (legacy), Next.js |
### Confidence Breakdown
| Token Group | Confidence | Notes |
|---|---|---|
| Typography scale (`--step-*`, `--text-*`) | **High** | Directly extracted CSS vars |
| Font families (`--font-stack-*`) | **High** | Extracted + confirmed by computed styles |
| Font weights (`--font-weight-*`) | **High** | Extracted + confirmed by computed styles |
| Spacing (`--space-*`) | **High** | Directly extracted |
| Radius (`--radius-*`) | **High** | Extracted + mined from computed border-radius census |
| Motion (`--duration-*`, `--ease-*`) | **High** | Extracted + confirmed by 248 element samples |
| Colour — brand surfaces/CTAs | **Medium–High** | Mined from computed bg-color census; 4 colours documented |
| Colour — semantic hover/active states | **Moderate** | Values referenced in CSS rules but not computed; values reconstructed from visual logic |
| Colour — checkbox/form states | **Low** | CSS var names extracted but values not resolved in computed pass |
| Modal radius (32px) | **High** | Directly from modal computed style |
| `--semantic-*` tokens | **Moderate** | Names extracted from CSS rules; values inferred — mark for manual extraction |
### Reconstruction Notes
- Semantic hover/active colour tokens (`--semantic-action-fill-primary-hover` etc.) are **referenced** in the extracted CSS interactive state rules but their resolved values were not present in the CSS custom property scan. Values in this document are **reconstructed** using the extracted base colours as anchors (darkened navy for hover, same for active with 0s transition). **These should be extracted manually** by inspecting the `:root` declarations on a live monzo.com page.
- `Maison Neue Web` and `Maison Neue Mono Web` font families are present in `@font-face` declarations — they appear to be legacy fonts from a previous design system version. Do not use in new components unless a specific page explicitly applies them.
- The `Oldschool` font family (`OldschoolGroteskCompact-ExtraBold, 800`) is a specialty display face. It appears in font declarations but was not detected in computed styles for any sampled elements. Usage scope is unknown — do not use as a general heading font.
- `--reach-dialog: 1` confirms the use of Reach UI's dialog component; its z-index behaviour is controlled by that library and should not be overridden.More from the gallery
Browse all kits →You may also like

Xero
MITClean, modern SaaS design system built on navy and mint green with sharp corners and purposeful typography for accounting and business software
00
saasfintechlightminimal

Netflix
MITDark, bold streaming platform design system with signature red accent and cinematic motion, built for large-scale video content discovery and subscription flows
00
darkboldecommlanding-page

Cash App
MITBold, high-contrast fintech design system built on Cash App's signature neon green and black palette, optimised for mobile payment interfaces and developer implementation
00
fintechmobilebolddark