Duolingo
MIT
Vibrant, playful design system with bright green accents and light blue surfaces, built for engaging educational and language-learning products
Layout StudioImport this kit into a Studio project and start editing.
CLI installRun it in any project. No account needed.
npx @layoutdesign/context install duolingo# layout.md — Duolingo Design System
---
## 0. Quick Reference
> Standalone — copy-paste into `CLAUDE.md` or `.cursorrules`
**Stack:** HTML/CSS + Bootstrap · Token source: reconstructed-from-computed (0 native CSS vars) · All tokens synthesised from computed styles.
**How to apply:** Use as `var(--duolingo-token-name)` in CSS, `style={{ prop: 'var(--duolingo-token-name)' }}` in JSX, or `bg-[var(--duolingo-token-name)]` in Tailwind.
```css
:root {
/* Colours */
--duolingo-accent: rgb(88, 204, 2); /* Duolingo green — primary CTA bg, h2 colour */
--duolingo-bg-surface: rgb(221, 244, 255); /* Light blue tint — page/section surface */
--duolingo-bg-app: rgb(16, 15, 62); /* Deep navy — dark section bg, button text */
--duolingo-text-primary: rgb(75, 75, 75); /* h1 body copy */
--duolingo-text-body: rgb(119, 119, 119); /* p body copy */
--duolingo-text-nav: rgb(60, 60, 60); /* nav items, badge text */
/* Typography — din-round is the brand typeface, feather for display h2 */
--duolingo-font-primary: 'din-round', sans-serif;
--duolingo-font-display: 'feather', sans-serif;
/* Radius */
--duolingo-radius-sm: 2px; /* badges, micro elements */
--duolingo-radius-md: 12px; /* buttons, cards */
/* Spacing (use these tokens — do NOT invent intermediate values) */
--duolingo-space-xs: 30px;
--duolingo-space-sm: 70px;
--duolingo-space-md: 96px;
--duolingo-space-lg: 101px;
/* Motion */
--duolingo-duration-fast: 0.2s;
--duolingo-ease-default: ease;
}
```
```tsx
// Primary CTA Button — correct token usage
<button
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '15px',
fontWeight: 700,
letterSpacing: '0.8px',
textTransform: 'uppercase',
color: 'var(--duolingo-bg-app)',
backgroundColor: 'var(--duolingo-accent)',
borderRadius: 'var(--duolingo-radius-md)',
padding: '14px 24px',
transition: `background-color var(--duolingo-duration-fast) var(--duolingo-ease-default)`,
border: 'none',
cursor: 'pointer',
}}
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'rgb(104, 182, 49)')}
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'var(--duolingo-accent)')}
>
Get Started
</button>
```
**Critical prohibitions:**
- **NEVER** use any font other than `din-round` (body/UI) or `feather` (display h2).
- **NEVER** use `border-radius` values other than `2px` or `12px`.
- **NEVER** hardcode hex/rgb colours — always use `var(--duolingo-*)` tokens.
- **NEVER** use `font-weight: 400` — the scale is `500` (regular) and `700` (bold) only.
- **NEVER** generate spacing values outside the defined `--duolingo-space-*` scale.
- **NEVER** use warm colours (red, orange, yellow) for primary surfaces or CTAs.
- **NEVER** use `Inter`, `Roboto`, or `Arial` as fallbacks — use `sans-serif` only.
**Full design system → see layout.md**
---
## 1. Design Direction & Philosophy
### Character & Aesthetic Intent
Duolingo's design is **playful-serious**: it uses bright, saturated green (`rgb(88, 204, 2)`) as its energising accent against a crisp light-blue surface, communicating approachability and optimism without sacrificing legibility. The deep navy (`rgb(16, 15, 62)`) grounds the palette and prevents the design from feeling juvenile.
Typography is opinionated and brand-owned: `din-round` (a rounded geometric sans) for all UI and body copy, and `feather` exclusively for hero/section headlines (`h2`). Both are custom webfonts loaded with `font-display: swap` — no system font substitution is acceptable.
### Mood
Encouraging, gamified, clean. The UI rewards the user. Whitespace is generous (section gaps of 70–101px). Text hierarchy is strict: only two font weights exist (500 and 700), which forces clarity through size contrast rather than weight variety.
### What This Design Explicitly Rejects
- **Warm palettes** — no reds, oranges, or yellows in primary surfaces.
- **Excessive curvature** — buttons are `12px` radius (rounded rectangle), NOT pill-shaped. Badges are `2px` (nearly sharp). Nothing in between.
- **Dense layouts** — section spacing is always ≥ 70px. Cramped UIs break the Duolingo feel.
- **Generic typography** — `Inter`, `Roboto`, `Arial`, or any system font is forbidden.
- **Muted or desaturated CTAs** — the accent green must remain fully saturated.
- **Dark-mode inversion by default** — the primary surface is light blue, not dark.
---
## 2. Colour System
### Tier 1 — Primitives
```css
:root {
/* Green family */
--primitive-green-500: rgb(88, 204, 2); /* brand green — Duolingo signature */
--primitive-green-600: rgb(104, 182, 49); /* darkened green — hover state for green CTAs */
/* Blue family */
--primitive-blue-50: rgb(221, 244, 255); /* lightest blue — page surface */
--primitive-blue-900: rgb(16, 15, 62); /* near-black navy — app dark bg */
--primitive-blue-focus: rgb(0, 99, 155); /* accessible focus ring blue */
/* Neutral/grey family */
--primitive-grey-900: rgb(75, 75, 75); /* darkest text — h1 */
--primitive-grey-700: rgb(119, 119, 119); /* body text */
--primitive-grey-600: rgb(60, 60, 60); /* nav, badge text */
--primitive-grey-500: rgb(128, 128, 128); /* shadow colour */
--primitive-grey-200: rgb(136, 136, 136); /* border on hover states */
--primitive-grey-50: rgb(86, 86, 86); /* muted link hover */
/* White / transparent */
--primitive-white: rgb(255, 255, 255);
--primitive-black: rgb(0, 0, 0);
/* Interactive (Google SSO ripple — third-party, not brand) */
--primitive-google-blue: rgba(66, 133, 244, 0.08); /* Google button hover overlay */
}
```
### Tier 2 — Semantic Aliases
```css
:root {
/* Surfaces */
--duolingo-bg-surface: var(--primitive-blue-50); /* main page/section background — reconstructed: high confidence */
--duolingo-bg-app: var(--primitive-blue-900); /* dark sections, button text overlay — reconstructed: low confidence (1 element) */
--duolingo-bg-white: var(--primitive-white); /* card and modal backgrounds */
/* Brand accent */
--duolingo-accent: var(--primitive-green-500); /* primary CTA background, h2 text — reconstructed: high confidence */
--duolingo-accent-hover: var(--primitive-green-600); /* CTA on hover — reconstructed: moderate confidence, inferred from #ot-sdk-btn hover */
/* Text */
--duolingo-text-primary: var(--primitive-grey-900); /* h1 headings — reconstructed: high confidence */
--duolingo-text-body: var(--primitive-grey-700); /* paragraph body copy — reconstructed: high confidence */
--duolingo-text-nav: var(--primitive-grey-600); /* nav links, badge labels — reconstructed: high confidence */
--duolingo-text-muted: var(--primitive-grey-500); /* secondary/placeholder text */
--duolingo-text-display: var(--duolingo-accent); /* h2 display headings use brand green */
/* Focus / Accessibility */
--duolingo-focus-ring: var(--primitive-blue-focus); /* 2px solid focus outline — reconstructed: high confidence */
/* Border */
--duolingo-border-default: var(--primitive-grey-200); /* subtle input/card borders */
}
```
### Tier 3 — Component Tokens
```css
:root {
/* Button — Primary */
--btn-primary-bg: var(--duolingo-accent);
--btn-primary-bg-hover: var(--duolingo-accent-hover);
--btn-primary-text: var(--duolingo-bg-app); /* deep navy on green */
--btn-primary-radius: var(--duolingo-radius-md); /* 12px */
/* Button — Secondary (nav "Log in" style) */
--btn-secondary-bg: transparent;
--btn-secondary-text: var(--duolingo-text-nav);
--btn-secondary-radius: var(--duolingo-radius-md);
/* Nav */
--nav-text: var(--duolingo-text-nav);
--nav-bg: var(--duolingo-bg-white);
/* Badge */
--badge-text: var(--duolingo-text-nav);
--badge-radius: var(--duolingo-radius-sm); /* 2px */
--badge-shadow: 0px 0px 5px 0px rgb(128, 128, 128);
}
```
### Colour Palette — At a Glance
| Token | Value | Usage |
|---|---|---|
| `--duolingo-accent` | `rgb(88, 204, 2)` | **Primary CTA bg, h2 text** |
| `--duolingo-bg-surface` | `rgb(221, 244, 255)` | Page surface background |
| `--duolingo-bg-app` | `rgb(16, 15, 62)` | Dark sections, CTA text |
| `--duolingo-text-primary` | `rgb(75, 75, 75)` | H1 headings |
| `--duolingo-text-body` | `rgb(119, 119, 119)` | Body paragraphs |
| `--duolingo-text-nav` | `rgb(60, 60, 60)` | Nav links, badges |
| `--duolingo-accent-hover` | `rgb(104, 182, 49)` | CTA hover state |
| `--duolingo-focus-ring` | `rgb(0, 99, 155)` | 2px focus outline |
---
## 3. Typography System
### Fonts
```css
@font-face {
font-family: 'din-round';
font-weight: 400; /* used for 500 and 700 — single file, weight simulated */
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'feather';
font-weight: 400;
font-style: normal;
font-display: swap;
}
```
> **`din-round`** is the universal UI font. **`feather`** is reserved exclusively for `h2` display headlines. NEVER swap them.
### Composite Typography Tokens
```css
:root {
/* ── Display / Hero ── */
/* Used for h1 on marketing hero sections (64px variant) */
--type-hero: {
font-family: 'din-round', sans-serif;
font-size: 64px; /* --duolingo-font-size-2xl */
font-weight: 700; /* --duolingo-font-weight-medium */
line-height: normal;
letter-spacing: normal;
color: var(--duolingo-text-primary);
}
/* ── Section Headline ── */
/* Used for h1 in secondary hero blocks (32px) */
--type-h1: {
font-family: 'din-round', sans-serif;
font-size: 32px; /* --duolingo-font-size-lg */
font-weight: 700;
line-height: normal;
letter-spacing: normal;
color: var(--duolingo-text-primary);
text-align: center;
}
/* ── Display Accent Headline ── */
/* h2 — feather font, brand green, used for section titles */
--type-h2: {
font-family: 'feather', sans-serif;
font-size: 48px; /* --duolingo-font-size-xl */
font-weight: 700;
line-height: normal;
letter-spacing: normal;
color: var(--duolingo-accent); /* rgb(88, 204, 2) */
text-align: start;
}
/* ── Body Copy ── */
/* p, main content paragraphs */
--type-body: {
font-family: 'din-round', sans-serif;
font-size: 17px; /* --duolingo-font-size-md */
font-weight: 500; /* --duolingo-font-weight-regular */
line-height: 24px; /* --duolingo-line-height-loose */
letter-spacing: normal;
color: var(--duolingo-text-body);
}
/* ── UI / Navigation ── */
/* Nav links, labels, badges */
--type-ui: {
font-family: 'din-round', sans-serif;
font-size: 17px; /* --duolingo-font-size-md */
font-weight: 500;
line-height: 20px; /* --duolingo-line-height-tight */
letter-spacing: normal;
color: var(--duolingo-text-nav);
}
/* ── Button Label ── */
/* All button text */
--type-button: {
font-family: 'din-round', sans-serif;
font-size: 15px; /* --duolingo-font-size-sm */
font-weight: 700;
line-height: normal;
letter-spacing: 0.8px;
text-transform: uppercase;
color: var(--duolingo-bg-app);
}
/* ── Caption / App Store labels ── */
/* Small labels like "Download on the" */
--type-caption: {
font-family: 'din-round', sans-serif;
font-size: 14px; /* --duolingo-font-size-xs */
font-weight: 500;
line-height: 20px; /* --duolingo-line-height-tight */
letter-spacing: normal;
}
}
```
### Typography Scale Summary
| Token | Family | Size | Weight | Line-height | Usage |
|---|---|---|---|---|---|
| `--type-hero` | din-round | **64px** | 700 | normal | Full-screen hero h1 |
| `--type-h1` | din-round | **32px** | 700 | normal | Section h1, centered |
| `--type-h2` | feather | **48px** | 700 | normal | Section titles, green |
| `--type-body` | din-round | 17px | 500 | 24px | Body paragraphs |
| `--type-ui` | din-round | 17px | 500 | 20px | Nav, badges, labels |
| `--type-button` | din-round | 15px | 700 | normal | Buttons, uppercase |
| `--type-caption` | din-round | 14px | 500 | 20px | App store, fine print |
### Font Weight Scale
| Token | Value | Usage |
|---|---|---|
| `--duolingo-font-weight-regular` | `500` | Body, UI, nav, captions |
| `--duolingo-font-weight-medium` | `700` | All headings, all buttons |
> **No `400` weight exists in this system.** The lightest weight is `500`.
---
## 4. Spacing & Layout
### Spacing Scale
```css
:root {
/* ── Spacing Tokens ── */
/* NOTE: These are extracted values — they do not follow a strict 4px grid.
30px ≈ 8×4 (rounded down), 70px ≈ 17×4 (nearest: 72px), 96px = 24×4, 101px ≈ 25×4.
Use tokens exactly as defined — do NOT round to 4px grid without design approval. */
--duolingo-space-xs: 30px; /* compact gap — internal card padding, small component spacing */
--duolingo-space-sm: 70px; /* section sub-gap — space between elements within a section */
--duolingo-space-md: 96px; /* section gap — vertical spacing between page sections */
--duolingo-space-lg: 101px; /* hero gap — largest vertical block separation */
/* Derived micro-spacing (inferred from button padding) */
--duolingo-space-2xs: 16px; /* button horizontal padding (extracted from button_primary: 0px 16px) */
--duolingo-space-3xs: 14px; /* button vertical padding (reconstructed: moderate confidence) */
}
```
### Grid System
```css
:root {
/* Container */
--duolingo-container-max: 1280px; /* inferred from breakpoint ceiling — reconstructed: moderate confidence */
--duolingo-container-padding: 30px; /* sides — aligns with --duolingo-space-xs */
/* Breakpoints — extracted from media queries */
--bp-xs: 400px; /* small phones */
--bp-sm: 530px; /* mid phones */
--bp-md: 769px; /* tablet portrait */
--bp-lg: 1024px; /* tablet landscape / small desktop */
--bp-xl: 1280px; /* large desktop */
/* Notable intermediate breakpoints (layout-specific, not semantic): */
/* 425px, 426px, 550px, 600px, 890px, 896px, 897px, 1023px */
}
```
### Layout Principles
```css
/* Navigation layout — extracted from role_navigation computed styles */
nav {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 0 226px; /* desktop centering — reconstructed: moderate confidence */
}
/* Primary button layout — extracted from button_primary computed styles */
button {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0 16px;
}
```
**Flex vs Grid decision rule:**
- Use **flex row + `justify-content: space-between`** for navigation and horizontal utility bars.
- Use **flex column** for stacked content within sections.
- Use **CSS grid** for multi-column feature or card layouts (column ratios TBD — no screenshot data available).
---
## 5. Page Structure & Layout Patterns
> Token source: layout digest (no screenshots). Inferred sections marked "(inferred)". Computed values anchored to extracted styles.
### 5.1 Section Map
| # | Section | Layout Type | Est. Height | Key Elements | Confidence |
|---|---|---|---|---|---|
| 1 | **Navigation / Header** | Flex row, space-between | ~80px | Logo, nav links, "Log in" + "Get started" CTAs | Extracted |
| 2 | **Hero** | Centered column | ~600px | H1 (64px, din-round), subhead (17px body), primary CTA (green), secondary CTA | Inferred |
| 3 | **Feature / Value Props** | Multi-column grid | ~500px | H2 (feather, green), body paragraphs, supporting imagery | Inferred |
| 4 | **Science / Methodology** | Alternating text+image | ~400px | H2, body copy, stat callouts | Inferred |
| 5 | **Motivation / Streak** | Full-width panel | ~400px | H2 ("stay motivated"), dark navy bg, illustration | Inferred |
| 6 | **App Download** | Flex row | ~300px | App store badges (14px caption), QR code or device mockup | Inferred |
| 7 | **Footer** | Multi-column + baseline | ~300px | Nav links (17px UI), legal, social icons | Inferred |
### 5.2 Layout Patterns
**Navigation:**
```
[Logo] [nav link] [nav link] [nav link] [nav link] [Log in] [Get Started ▶]
← justify-content: space-between | margin: 0 226px (desktop) | align-items: center →
```
- The nav is a flex row with `justify-content: space-between`. The logo sits left, the CTA buttons sit right.
- CTA "Get Started" uses `--duolingo-accent` background, `--duolingo-bg-app` text, `border-radius: 12px`.
**Hero Section (inferred):**
- Single centered column, `text-align: center` (confirmed on h1 computed style).
- H1 at `64px` / `700` weight in `din-round`, followed by 17px body paragraph.
- Primary CTA button: green (`rgb(88, 204, 2)`) background, navy text, `12px` radius, uppercase `15px` label.
- Vertical gap between hero elements: `--duolingo-space-xs` (30px).
**Feature Sections (inferred):**
- H2 in `feather`, `48px`, `rgb(88, 204, 2)`, `text-align: start` — confirmed left-aligned.
- Body copy `17px` / `500` weight in `din-round`, `rgb(119, 119, 119)`.
- Section vertical spacing: `--duolingo-space-md` (96px) between sections, `--duolingo-space-sm` (70px) between elements within a section.
**Dark Section (inferred from `--duolingo-bg-app`):**
- Background: `rgb(16, 15, 62)` (deep navy). Only 1 element uses this colour — likely a full-width highlight band.
- Text likely inverts to white — `[TBD - extract manually]`.
### 5.3 Visual Hierarchy
- **Most prominent element:** H2 headings in `feather` + Duolingo green — they command attention before body copy.
- **CTA placement:** Top-right (nav) + center-hero. Primary CTA is always the green button.
- **Whitespace rhythm:** Generous — 96–101px between major sections, 70px within sections, 30px for compact internal gaps.
- **Colour as hierarchy:** Green = action/brand emphasis. Navy = structural depth. Light blue = surface rest state.
### 5.4 Content Patterns
1. **Headline → Body → CTA:** Every section leads with H2 (green, feather), followed by 17px body copy, then an action element (button or link).
2. **Left-aligned sections:** H2 `text-align: start` — sections are NOT centered-headline layouts (only h1 is `text-align: center`).
3. **App store badges:** Consistently use `--type-caption` (14px, din-round) for "Download on the" / "Get it on" labels.
4. **Nav CTAs:** Two buttons always present — secondary (outline/ghost style) and primary (green filled). Both use `border-radius: 12px`.
---
## 6. Component Patterns
### 6.1 Primary Button
**Anatomy:** `[button] → [uppercase text label]`
**Token-to-property mapping:**
| State | Background | Text | Border | Shadow | Cursor |
|---|---|---|---|---|---|
| Default | `--duolingo-accent` (`rgb(88,204,2)`) | `--duolingo-bg-app` | none | none | pointer |
| Hover | `rgb(104, 182, 49)` | `--duolingo-bg-app` | none | none | pointer |
| Focus | `--duolingo-accent` | `--duolingo-bg-app` | outline `2px solid rgb(0,99,155)` | none | pointer |
| Active | `rgb(72, 170, 1)` (darkened) | `--duolingo-bg-app` | none | none | pointer |
| Disabled | `rgba(88,204,2,0.4)` | `rgba(16,15,62,0.4)` | none | none | not-allowed |
```tsx
// PrimaryButton.tsx — production-ready, all states
import { ButtonHTMLAttributes, forwardRef } from 'react';
interface PrimaryButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean;
}
const PrimaryButton = forwardRef<HTMLButtonElement, PrimaryButtonProps>(
({ children, loading, disabled, ...props }, ref) => {
const isDisabled = disabled || loading;
return (
<button
ref={ref}
disabled={isDisabled}
{...props}
style={{
// Layout
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: '14px 24px',
gap: '8px',
// Typography — --type-button composite
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '15px',
fontWeight: 700,
letterSpacing: '0.8px',
textTransform: 'uppercase',
textDecoration: 'none',
// Colour
color: isDisabled
? 'rgba(16, 15, 62, 0.4)'
: 'var(--duolingo-bg-app)',
backgroundColor: isDisabled
? 'rgba(88, 204, 2, 0.4)'
: 'var(--duolingo-accent)',
// Shape
borderRadius: 'var(--duolingo-radius-md)',
border: 'none',
// Interaction
cursor: isDisabled ? 'not-allowed' : 'pointer',
transition: `background-color var(--duolingo-duration-fast) var(--duolingo-ease-default),
opacity var(--duolingo-duration-fast) var(--duolingo-ease-default)`,
...props.style,
}}
onMouseEnter={e => {
if (!isDisabled) {
e.currentTarget.style.backgroundColor = 'rgb(104, 182, 49)';
}
}}
onMouseLeave={e => {
if (!isDisabled) {
e.currentTarget.style.backgroundColor = 'var(--duolingo-accent)';
}
}}
onMouseDown={e => {
if (!isDisabled) {
e.currentTarget.style.backgroundColor = 'rgb(72, 170, 1)';
}
}}
onMouseUp={e => {
if (!isDisabled) {
e.currentTarget.style.backgroundColor = 'rgb(104, 182, 49)';
}
}}
onFocus={e => {
e.currentTarget.style.outline = '2px solid var(--duolingo-focus-ring)';
e.currentTarget.style.outlineOffset = '2px';
}}
onBlur={e => {
e.currentTarget.style.outline = 'none';
}}
>
{loading ? (
<span aria-label="Loading" role="status" style={{ opacity: 0.7 }}>
···
</span>
) : (
children
)}
</button>
);
}
);
PrimaryButton.displayName = 'PrimaryButton';
export default PrimaryButton;
```
---
### 6.2 Navigation Bar
**Anatomy:** `[nav] → [logo] + [nav-links group] + [cta-group: secondary + primary button]`
**Token-to-property mapping:**
| State | Link colour | Link decoration | CTA |
|---|---|---|---|
| Default | `--duolingo-text-nav` (`rgb(60,60,60)`) | none | Green button |
| Hover (link) | `rgb(86, 86, 86)` | none | — |
| Focus (link) | `rgb(60,60,60)` | outline `2px solid rgb(0,0,0)` | — |
| Active (link) | `rgb(40,40,40)` (inferred) | none | — |
```tsx
// NavBar.tsx
const NavBar = () => (
<nav
role="navigation"
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 226px',
backgroundColor: 'var(--duolingo-bg-white)',
height: '80px',
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '17px',
fontWeight: 500,
lineHeight: '20px',
}}
>
{/* Logo */}
<a href="/" aria-label="Duolingo home">
{/* SVG logo here */}
</a>
{/* Nav Links */}
<ul style={{ display: 'flex', gap: '32px', listStyle: 'none', margin: 0, padding: 0 }}>
{['Courses', 'Mission', 'Approach'].map(label => (
<li key={label}>
<a
href={`/${label.toLowerCase()}`}
style={{
color: 'var(--duolingo-text-nav)',
textDecoration: 'none',
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '17px',
fontWeight: 500,
lineHeight: '22px',
transition: `color var(--duolingo-duration-fast) var(--duolingo-ease-default)`,
}}
onMouseEnter={e => (e.currentTarget.style.color = 'rgb(86, 86, 86)')}
onMouseLeave={e => (e.currentTarget.style.color = 'var(--duolingo-text-nav)')}
>
{label}
</a>
</li>
))}
</ul>
{/* CTA Group */}
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<button
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '15px',
fontWeight: 700,
letterSpacing: '0.8px',
textTransform: 'uppercase',
color: 'var(--duolingo-text-nav)',
backgroundColor: 'transparent',
border: '2px solid var(--duolingo-text-nav)',
borderRadius: 'var(--duolingo-radius-md)',
padding: '10px 20px',
cursor: 'pointer',
}}
>
Log In
</button>
<PrimaryButton>Get Started</PrimaryButton>
</div>
</nav>
);
```
---
### 6.3 Badge
**Anatomy:** `[div.badge] → [text content]`
**Token-to-property mapping:**
| State | Background | Shadow | Border |
|---|---|---|---|
| Default | transparent | `rgb(128,128,128) 0px 0px 5px 0px` | `2px` radius |
| Hover | transparent | — | — |
| Focus | outline `2px solid rgb(0,0,0)` | — | — |
```tsx
// Badge.tsx
const Badge = ({ children }: { children: React.ReactNode }) => (
<div
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '17px',
fontWeight: 500,
lineHeight: '20px',
color: 'var(--duolingo-text-nav)',
borderRadius: 'var(--duolingo-radius-sm)', /* 2px — NEVER use 12px here */
boxShadow: 'var(--duolingo-shadow-badge)',
padding: '8px 12px',
display: 'inline-block',
}}
>
{children}
</div>
);
```
---
### 6.4 Text Input
**Anatomy:** `[label] + [input]`
**Token-to-property mapping:**
| State | Border | Outline | Background |
|---|---|---|---|
| Default | `1px solid rgb(136,136,136)` | none | white |
| Hover | `1px solid rgb(136,136,136)` | none | white |
| Focus | `1px solid rgb(0,0,0)` | none (outline `0px`) | white |
| Disabled | — | none | `rgba(0,0,0,0.05)` (inferred) |
| Error | `[TBD - extract manually]` | — | — |
```tsx
// TextInput.tsx
const TextInput = ({
label,
disabled,
error,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & { label: string; error?: string }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '15px',
fontWeight: 700,
color: 'var(--duolingo-text-nav)',
}}
>
{label}
</label>
<input
disabled={disabled}
{...props}
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '17px',
fontWeight: 500,
color: disabled ? 'var(--duolingo-text-muted)' : 'var(--duolingo-text-primary)',
backgroundColor: disabled ? 'rgba(0,0,0,0.05)' : 'var(--duolingo-bg-white)',
border: error
? '1px solid rgb(220, 38, 38)'
: '1px solid var(--duolingo-border-default)',
borderRadius: 'var(--duolingo-radius-md)',
padding: '12px 16px',
outline: 'none',
transition: `border-color var(--duolingo-duration-fast) var(--duolingo-ease-default)`,
cursor: disabled ? 'not-allowed' : 'text',
opacity: disabled ? 0.7 : 1,
...props.style,
}}
onFocus={e => {
e.currentTarget.style.borderColor = 'rgb(0, 0, 0)';
}}
onBlur={e => {
e.currentTarget.style.borderColor = error
? 'rgb(220, 38, 38)'
: 'var(--duolingo-border-default)';
}}
/>
{error && (
<span
style={{
fontFamily: 'var(--duolingo-font-primary)',
fontSize: '14px',
fontWeight: 500,
color: 'rgb(220, 38, 38)',
}}
>
{error}
</span>
)}
</div>
);
```
---
## 7. Elevation & Depth
```css
:root {
/* Shadow tokens */
--duolingo-shadow-badge: rgb(128, 128, 128) 0px 0px 5px 0px;
/* Used on: badge elements. Soft ambient glow — not directional. */
--duolingo-shadow-none: none;
/* Used on: buttons, nav, body, headings — flat design by default. */
/* Elevation scale (reconstructed: moderate confidence — only 1 shadow value found) */
--duolingo-elevation-0: none; /* flush with surface */
--duolingo-elevation-1: rgb(128, 128, 128) 0px 0px 5px 0px; /* badge, soft float */
--duolingo-elevation-2: [TBD - extract manually]; /* card/modal — not yet extracted */
/* Border tokens */
--duolingo-border-default: 1px solid rgb(136, 136, 136); /* input default */
--duolingo-border-focus: 1px solid rgb(0, 0, 0); /* input focus */
--duolingo-border-transparent: 1px solid transparent; /* removes border visually */
/* Z-index scale (reconstructed: moderate confidence) */
--z-base: 0; /* page content */
--z-badge: 10; /* floating badge */
--z-nav: 100; /* sticky navigation */
--z-modal: 200; /* modals, cookie banners */
--z-toast: 300; /* toasts / system alerts */
}
```
**Layering principles:**
- The UI is predominantly **flat** — most elements have `box-shadow: none`.
- Depth is communicated through **colour contrast** (navy vs light blue), not shadows.
- Shadows appear only on floating/overlapping elements (badges, modals).
---
## 8. Motion
```css
:root {
/* Duration tokens */
--duolingo-duration-fast: 0.2s; /* standard micro-interaction (buttons, links, badge slide) */
--duolingo-duration-medium: 0.3s; /* badge position transition (extracted: `transition: right 0.3s`) */
--duolingo-duration-slow: [TBD - extract manually]; /* page-level transitions — not extracted */
/* Easing tokens */
--duolingo-ease-default: ease; /* applied to 66 elements — universal easing */
--duolingo-ease-linear: linear; /* [TBD - extract manually] */
/* Composite motion tokens */
--duolingo-transition-button: background-color var(--duolingo-duration-fast) var(--duolingo-ease-default),
opacity var(--duolingo-duration-fast) var(--duolingo-ease-default);
--duolingo-transition-badge: right var(--duolingo-duration-medium) var(--duolingo-ease-default);
--duolingo-transition-link: color var(--duolingo-duration-fast) var(--duolingo-ease-default);
}
```
### When to Animate
| Trigger | Duration | Properties | Token |
|---|---|---|---|
| Button hover/active | `0.2s` | `background-color` | `--duolingo-transition-button` |
| Link hover | `0.2s` | `color` | `--duolingo-transition-link` |
| Badge position (slide-in) | `0.3s` | `right` | `--duolingo-transition-badge` |
| Focus ring appear | `0.2s` | `outline` | `--duolingo-transition-button` |
### When NOT to Animate
- **Layout shifts** — do not animate `width`, `height`, or `margin` changes (causes reflow jank).
- **Page navigation** — no route-transition animations (not part of this design system).
- **Gamification animations** — streak/XP celebrations are feature-level, not system-level; handle separately from these tokens.
- **Respect `prefers-reduced-motion`:** Wrap all transitions:
```css
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}
```
---
## 9. Anti-Patterns & Constraints
**Rule 1: Never hardcode colour values inline.**
→ **Why it fails:** When an AI sees `color: rgb(88, 204, 2)` in context, it will reproduce the literal value throughout the component tree, bypassing the token system. If brand green ever shifts (e.g. accessibility audit), every hardcoded instance breaks silently.
→ **Do instead:** Always use `color: var(--duolingo-accent)`. Define the value once in `:root`.
---
**Rule 2: Never use `font-weight: 400`.**
→ **Why it fails:** AI agents default to `400` as the "normal" weight. But Duolingo's body weight is `500`, and `400` renders visibly lighter on `din-round`, creating a typographic inconsistency that looks like an incomplete render or a font-loading failure.
→ **Do instead:** Use `font-weight: 500` (`--duolingo-font-weight-regular`) for all body/UI, `700` (`--duolingo-font-weight-medium`) for headings and buttons. Never `400`.
---
**Rule 3: Never use `Inter`, `Roboto`, `Arial`, or any system font as a primary or fallback font.**
→ **Why it fails:** AI coding assistants trained on common codebases default to `Inter` or `system-ui`. Rendering `din-round` content in `Inter` collapses Duolingo's brand identity — the rounded letter forms are structurally central to the visual personality.
→ **Do instead:** Font stack is always `'din-round', sans-serif` (body/UI) or `'feather', sans-serif` (h2 display). The `sans-serif` fallback is intentional as a loading placeholder only.
---
**Rule 4: Never use `border-radius` values other than `2px` or `12px`.**
→ **Why it fails:** AI agents often apply `8px` as a "reasonable default" or `9999px` for pill shapes. Duolingo's system has exactly two radii: `2px` (badges/micro) and `12px` (buttons/cards). Any other value is off-system and visually incorrect.
→ **Do instead:** Use `var(--duolingo-radius-sm)` (2px) for small/flat elements, `var(--duolingo-radius-md)` (12px) for interactive controls and containers.
---
**Rule 5: Never construct Tailwind class names dynamically.**
→ **Why it fails:** Tailwind's JIT compiler purges classes that aren't statically detectable at build time. `bg-[${color}]` or `` `text-${size}` `` will compile to nothing — the style silently vanishes in production.
→ **Do instead:** Use `var(--duolingo-*)` tokens with inline styles or a CSS-in-JS approach for dynamic values. Static Tailwind classes (e.g. `bg-[var(--duolingo-accent)]`) are fine.
---
**Rule 6: Never omit hover, focus, and disabled states from interactive components.**
→ **Why it fails:** AI generates the `default` state only when no state table is provided. Duolingo's UI is gamified and interaction-dense — missing focus rings fail WCAG 2.1 AA, missing hover feedback breaks the energetic feel, and a greyed-out disabled button without `cursor: not-allowed` confuses users.
→ **Do instead:** Implement all five states (default, hover, focus, active, disabled) for every button and input, using the state table in Section 6.
---
**Rule 7: Never use `position: absolute` as a layout mechanism for multi-element sections.**
→ **Why it fails:** AI reaching for absolute positioning to layer text over images will break at responsive breakpoints — the positioned elements overflow or collapse since absolute elements are taken out of flow.
→ **Do instead:** Use `display: flex` with `justify-content: space-between` (nav pattern) or `display: grid` for overlapping content. Use `position: absolute` only for decorative overlays with known dimensions (e.g. mascot character overlapping a card corner).
---
**Rule 8: Never use spacing values not present in the `--duolingo-space-*` token set.**
→ **Why it fails:** 3 off-grid values were found in extraction (30px, 70px, 101px) — these are intentional design values, not accidents. When AI "normalises" them to `32px`, `72px`, `100px`, it breaks the precise whitespace rhythm that defines Duolingo's airy layout.
→ **Do instead:** Use `var(--duolingo-space-xs)` through `var(--duolingo-space-lg)` exactly. If a spacing need doesn't fit the scale, flag it for design review — don't interpolate.
---
**Rule 9: Never use `!important` to override component styles.**
→ **Why it fails:** Duolingo's Bootstrap base means `!important` chains accumulate quickly. One override triggers a cascade of counter-overrides — the codebase becomes unmaintainable and the token system loses authority.
→ **Do instead:** Increase CSS specificity with compound selectors (e.g. `button.duolingo-btn`) or use CSS Modules / CSS-in-JS scoping to isolate component styles without `!important`.
---
**Rule 10: Never apply `--duolingo-bg-app` (navy, `rgb(16, 15, 62)`) as a text colour on dark backgrounds.**
→ **Why it fails:** This navy is used as the text colour on green CTA buttons specifically. AI may reuse it as general "dark text" — but on the dark surface sections (which also use this navy as background), it produces invisible black-on-black text.
→ **Do instead:** On dark (`--duolingo-bg-app`) backgrounds, text should invert to white `rgb(255,255,255)` `[TBD - extract manually — no dark-mode text token extracted]`. Use `--duolingo-text-primary` (grey-900) only on light surfaces.
---
## 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 (34) */
--brand-surface-1: rgb(221, 244, 255); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--brand-surface-2: rgb(88, 204, 2); /* Brand surface, dominant on 2 elements — e.g. "About usCoursesMissionApproach" /* mined from computed styles */ */
--brand-surface-3: rgb(16, 15, 62); /* Brand surface, dominant on 1 element — e.g. "Try 1 week free" /* mined from computed styles */ */
--duolingo-accent: rgb(88, 204, 2);
--duolingo-bg-surface: rgb(221, 244, 255);
--duolingo-bg-app: rgb(16, 15, 62);
--duolingo-text-primary: rgb(75, 75, 75);
--duolingo-text-body: rgb(119, 119, 119);
--duolingo-text-nav: rgb(60, 60, 60);
--primitive-green-500: rgb(88, 204, 2);
--primitive-green-600: rgb(104, 182, 49);
--primitive-blue-50: rgb(221, 244, 255);
--primitive-blue-900: rgb(16, 15, 62);
--primitive-blue-focus: rgb(0, 99, 155);
--primitive-grey-900: rgb(75, 75, 75);
--primitive-grey-700: rgb(119, 119, 119);
--primitive-grey-600: rgb(60, 60, 60);
--primitive-grey-500: rgb(128, 128, 128);
--primitive-grey-200: rgb(136, 136, 136);
--primitive-grey-50: rgb(86, 86, 86);
--primitive-white: rgb(255, 255, 255);
--primitive-black: rgb(0, 0, 0);
--primitive-google-blue: rgba(66, 133, 244, 0.08);
--duolingo-bg-white: var(--primitive-white);
--duolingo-border-default: 1px solid rgb(136, 136, 136);
--btn-primary-bg: var(--duolingo-accent);
--btn-primary-bg-hover: var(--duolingo-accent-hover);
--btn-secondary-bg: transparent;
--nav-bg: var(--duolingo-bg-white);
--duolingo-elevation-1: rgb(128, 128, 128) 0px 0px 5px 0px;
--duolingo-border-focus: 1px solid rgb(0, 0, 0);
--duolingo-border-transparent: 1px solid transparent;
--duolingo-accent-hover: rgb(104, 182, 49);
--duolingo-focus-ring: rgb(0, 99, 155);
/* Typography (30) */
--font-size-xs: 14px; /* 2 elements — e.g. span "Download on the", span "Get it on" /* mined from computed styles */ */
--font-size-sm: 15px; /* 68 elements — e.g. span "Site language: Engli", span "Get started", span "I ALREADY HAVE AN AC" /* mined from computed styles */ */
--font-size-md: 17px; /* 115 elements — e.g. p "Learning with Duolin", p "We use a combination", p "We make it easy to f" /* mined from computed styles */ */
--font-size-lg: 32px; /* 1 element — e.g. h1 "The free, fun, and e" /* mined from computed styles */ */
--font-size-xl: 48px; /* 9 elements — e.g. h2 "free. fun. effective", h2 "backed by science", h2 "stay motivated" /* mined from computed styles */ */
--font-size-2xl: 64px; /* 2 elements — e.g. h1 "learn anytime, anywh", h1 "learn a language wit" /* mined from computed styles */ */
--font-weight-regular: 500; /* 110 elements — e.g. p "Learning with Duolin", p "We use a combination", p "We make it easy to f" /* mined from computed styles */ */
--font-weight-medium: 700; /* 87 elements — e.g. h1 "The free, fun, and e", h1 "learn anytime, anywh", h1 "learn a language wit" /* mined from computed styles */ */
--line-height-tight: 20px; /* 148 elements — e.g. span "English", span "Spanish", span "French" /* mined from computed styles */ */
--line-height-normal: 22px; /* 9 elements — e.g. a "Courses", a "Mission", a "Approach" /* mined from computed styles */ */
--line-height-loose: 24px; /* 9 elements — e.g. p "Learning with Duolin", p "We use a combination", p "We make it easy to f" /* mined from computed styles */ */
--duolingo-font-primary: 'din-round', sans-serif;
--duolingo-font-display: 'feather', sans-serif;
--duolingo-text-muted: var(--primitive-grey-500);
--duolingo-text-display: var(--duolingo-accent);
--btn-primary-text: var(--duolingo-bg-app);
--btn-secondary-text: var(--duolingo-text-nav);
--nav-text: var(--duolingo-text-nav);
--badge-text: var(--duolingo-text-nav);
--duolingo-font-weight-regular: 500;
--duolingo-font-weight-medium: 700;
--duolingo-font-size-xs: 14px;
--duolingo-font-size-sm: 15px;
--duolingo-font-size-md: 17px;
--duolingo-font-size-lg: 32px;
--duolingo-font-size-xl: 48px;
--duolingo-font-size-2xl: 64px;
--duolingo-line-height-tight: 20px;
--duolingo-line-height-normal: 22px;
--duolingo-line-height-loose: 24px;
/* Spacing (17) */
--space-xs: 30px; /* 3 elements — e.g. nav .zU2RQ, nav .zU2RQ, nav .zU2RQ /* mined from computed styles */ */
--space-sm: 70px; /* 1 element — e.g. header ._39290 /* mined from computed styles */ */
--space-md: 96px; /* 4 elements — e.g. section .uU0-M, section .uU0-M, section ._3dG3I /* mined from computed styles */ */
--space-lg: 101px; /* 21 elements — e.g. section ._3k9io, section ._3k9io, section ._3k9io /* mined from computed styles */ */
--duolingo-space-xs: 30px;
--duolingo-space-sm: 70px;
--duolingo-space-md: 96px;
--duolingo-space-lg: 101px;
--duolingo-space-2xs: 16px;
--duolingo-space-3xs: 14px;
--duolingo-container-max: 1280px;
--duolingo-container-padding: 30px;
--bp-xs: 400px;
--bp-sm: 530px;
--bp-md: 769px;
--bp-lg: 1024px;
--bp-xl: 1280px;
/* Radius (7) */
--radius-sm: 2px; /* 1 element — e.g. div .grecaptcha-badge /* mined from computed styles */ */
--radius-md: 12px; /* 5 elements — e.g. button ._2V6ug "I ALREADY HAVE AN AC", button ._1rcV8 "Try 1 week free", button "REJECT ALL" /* mined from computed styles */ */
--duolingo-radius-sm: 2px;
--duolingo-radius-md: 12px;
--btn-primary-radius: var(--duolingo-radius-md);
--btn-secondary-radius: var(--duolingo-radius-md);
--badge-radius: var(--duolingo-radius-sm);
/* Effects (3) */
--badge-shadow: 0px 0px 5px 0px rgb(128,128,128);
--duolingo-shadow-badge: rgb(128,128,128) 0px 0px 5px 0px;
--duolingo-shadow-none: none;
/* Motion (2) */
--duration-fast: 0.2s; /* 1 element — e.g. button /* mined from computed styles */ */
--ease-default: ease; /* 66 elements — e.g. button, button, button /* mined from computed styles */ */
```
## Appendix B: Token Source Metadata
| Property | Value |
|---|---|
| **Token source** | `reconstructed-from-computed` |
| **Extraction method** | Computed styles from DOM elements (h1, h2, body, button, nav, badge, link) |
| **Native CSS custom properties** | **0 found** — no `--var` definitions detected in the site's stylesheets |
| **Confidence level** | Low overall; individual token confidence noted inline |
| **Detected libraries** | Bootstrap (base layout utility) |
| **Clustering method** | Colours grouped by hue family (green, blue, neutral greys); spacing grouped by extracted values (no 4px grid normalisation applied — values preserved as-extracted); radius clustered by distinct values (2px, 12px) |
| **Radius note** | No pill-shaped buttons detected (largest radius = 12px). The 2px radius is from reCAPTCHA badge (third-party element) but aligns with Duolingo's sharp micro-element aesthetic. |
| **Typography note** | `din-round` and `feather` are custom webfonts loaded via `@font-face` with `font-display: swap`. Both declared at `font-weight: 400` in the `@font-face` rule, but computed weights show `500` and `700` throughout — font files likely contain variable weight or separate files per weight not captured in extraction. |
| **Spacing note** | Values 30px, 70px, 101px do not conform to a 4px grid. Preserved exactly as extracted. Do not normalise without design sign-off. |
| **Breakpoints note** | 13 breakpoints detected — unusual density. Many (425px, 426px, 896px, 897px) are paired 1px apart, suggesting layout pivot points rather than semantic breakpoints. The 5 semantic breakpoints (400, 530, 769, 1024, 1280) are most reliable for layout decisions. |
| **Interactive states** | Most state data sourced from OneTrust (cookie consent SDK) CSS rules — **not Duolingo brand styles**. Duolingo-native button states have low extraction confidence and should be validated manually. |
| **Missing data** | Dark-section text colour (white text on `--duolingo-bg-app`) not extracted. Error state colours not confirmed. Card component not isolated. Hero image/illustration layout not confirmed. Mark all `[TBD]` fields for manual extraction. |More from the gallery
Browse all kits →You may also like

Contra
MITPlayful yet professional design system with teal and purple accents, bold typography, and vibrant accent surfaces—built for modern SaaS platforms and landing pages
03
lightboldsaaslanding-page

DoorDash
MITClean, accessible food-delivery system with DoorDash's signature red accent, light neutral palette, and system-font typography—built for rapid product development
03
lightecommsaasmobile

Mercedes-Benz
MITLuxe, minimalist design system with sharp black-and-white contrast and serif typography, crafted for premium automotive and lifestyle brands
05
darkminimalboldecomm