Duolingo Design System
Overview
Duolingo's visual identity is a masterclass in approachable education design — it balances the rigor of structured learning with the warmth of a game interface. The system rests on a near-pure {colors.canvas} white foundation that lets content breathe while signature {colors.primary} lime-green acts as the single source of brand energy, deployed almost exclusively on conversion-driving buttons. Typography leans heavily into a custom geometric sans stack (din-round paired with feather for display moments), delivering confidence through weight rather than size alone — body copy sits at a generous 17px with medium weight, while headlines punch at 700.
The shape vocabulary is distinctly rounded: {rounded.DEFAULT} at 12px defines the primary button language, creating a soft, tactile quality that signals "this is safe to tap." This roundsness extends into the illustration style visible in the hero composition, where Duo (the owl mascot) and learner avatars float in a loose cluster with no hard edges anywhere. Color beyond the primary green is deliberately restrained — {colors.accent} sky-blue serves as the sole secondary action/link color, while {colors.muted} grays handle hierarchy without introducing new hues. The overall effect is one of cheerful minimalism: every pixel earns its place, and every interactive element feels like a small reward waiting to happen.
The system operates in what might be called a "gamified editorial" mode — it's not a dashboard and not a marketing brochure, but something between: instructional content wrapped in game-like affordances. Sub-systems are minimal here; the landing page presents essentially one dialect (acquisition/onboarding) rather than the multi-mode complexity of the full app experience.
Key Characteristics
- Canvas-first design with
{colors.canvas}(#ffffff) as the dominant surface, creating high-contrast readability - Single brand voltage color
{colors.primary}(#58cc02) applied exclusively to{component.button-primary}and brand marks - Rounded geometry throughout —
{rounded.DEFAULT}(12px) on all primary interactive elements - Bold type weights (700) on both
{typography.display-lg}headings and{typography.button}labels - Uppercase
{typography.label-uppercase}treatment on utility actions like{component.language-selector-button} - Dual-font system:
featherfor{typography.display-xl}moments,din-roundfor everything else - Flat elevation model — shadows are nearly absent; depth comes from color contrast only
- Character-driven illustration as primary visual communication, not photography
- Generous
{spacing.section}(96px) vertical rhythm between major bands - Sky-blue
{colors.accent}(#1cb0f6) reserved for secondary/tertiary links like{component.button-secondary}
Colors
Brand & Accent
- Primary Green (
{colors.primary}— #58cc02): The signature Duolingo lime-green. Used exclusively on{component.button-primary}, the logo mark, and success states. This is the only color that feels "branded" in the traditional sense. - Primary Hover (
{colors.primary-hover}— #46a302): Darkened primary state for hover interaction on green buttons. - Primary Active (
{colors.primary-active}— #3e8f01): Pressed/depressed state of primary green, providing tactile feedback. - Primary Disabled (
{colors.primary-disabled}— #a5cd8f): Muted desaturated green for disabled primary buttons. - Accent Blue (
{colors.accent}— #1cb0f6): Sky-blue used for secondary actions, links, and informational highlights. Appears on{component.button-secondary}and inline links. - Accent Hover (
{colors.accent-hover}— #1499d9): Darker blue for hover states on accent-colored elements. - Secondary Yellow (
{colors.secondary}— #ffc800): Golden yellow reserved for badges, streak counters, and achievement indicators like{component.badge-pill}.
Surface
- Canvas (
{colors.canvas}— #ffffff): Pure white base layer. The default background for{component.hero-band},{component.top-nav}, and virtually all containers. - Surface Soft (
{colors.surface-soft}— #f7f7f7): Near-white neutral for subtle differentiation. Used on hover states like{component.language-course-item-hover}and{component.footer-nav}. - Surface Elevated (
{colors.surface-elevated}— #ffffff): Elevated cards share the canvas color; separation comes from shadow or border, not background shift. - Surface Warm (
{colors.surface-warm}— #fff9e6): Extremely subtle warm tint for premium/gamification contexts (Super Duolingo treatments).
Text / Ink
- Ink (
{colors.ink}— #3c3c3c): Near-black primary reading color. Used for{component.hero-headline}, body text in high-importance contexts, and{component.button-primary}text. - Body Gray (
{colors.body}— #777777): Medium-gray for standard paragraph text, descriptions, and supporting content like{component.hero-subheadline}. - Heading Gray (
{colors.heading}— #4b4b4b): Slightly softer than ink, used specifically on h1-level headlines for reduced harshness. - Muted (
{colors.muted}— #afafaf): Light gray for de-emphasized utility text —{component.language-selector-button}, placeholder text, timestamps. - Muted Strong (
{colors.muted-strong}— #888888): Between body and muted for secondary metadata. - On Primary (
{colors.on-primary}— #3c3c3c): Dark text rendered atop{colors.primary}green surfaces — satisfies contrast requirements against lime backgrounds. - On Dark (
{colors.on-dark}— #ffffff): White text for any dark-surface contexts (not prominent on this landing page).
Hairlines & Borders
- Hairline (
{colors.hairline}— #e5e5e5): Very light divider for subtle separation lines and disabled borders. - Border Default (
{colors.border-default}— #c1c1c1): Standard form field border color as seen on{component.input-text}. - Border Strong (
{colors.border-strong}— #afafaf): Medium-strength border for focused or active states.
Semantic
- Error Red (
{colors.error}— #ff4b4b): Alert/error messaging color. - Success (
{colors.success}— #58cc02): Maps to primary green — Duolingo uses brand green for success confirmation. - Warning (
{colors.warning}— #ffc800): Caution/warning state, maps to secondary yellow. - Info (
{colors.info}— #1cb0f6): Informational callouts, maps to accent blue. - Link (
{colors.link}— #1cb0f6): Default hyperlink color, identical to accent blue. - Link Active (
{colors.link-active}— #042c60): Deep navy for visited or active link states. - Link Visited (
{colors.link-visited}— #0000ee): Standard browser-blue fallback for visited links.
Typography
Font Family
Duolingo employs a two-face typographic system built around proprietary fonts. DIN Round (din-round, sans-serif) carries 95% of the workload — it's the workhorse face for body text, UI labels, buttons, navigation, and even mid-tier headlines. Its geometric construction with rounded terminals mirrors the physical roundness of the UI's {rounded DEFAULT} corners, creating a cohesive shape language between letterform and container. Feather (feather, sans-serif) appears exclusively in display contexts — currently observed only on the {typography.display-xl} 48px headline tier, where its slightly more expressive character adds warmth to the largest text on screen. The fallback chain terminates in sans-serif, ensuring graceful degradation if the custom webfonts fail to load.
The font stack reveals a deliberate choice: DIN Round is a German industrial typeface (originally for technical signage) softened by rounded stroke endings, which perfectly encodes Duolingo's "structured play" philosophy. Feather complements it with more open counters and friendlier curves, making it ideal for the big welcoming moment when users first read "The free, fun, and effective way to learn a language!"
Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| {typography.display-xl} | 48px | 700 | 1.15 | -0.01em | Hero headline (feather) |
| {typography.display-lg} | 32px | 700 | 1.25 | -0.01em | Section headings (din-round) |
| {typography.title-md} | 22px | 700 | 1.35 | 0 | Card titles, sub-section headers |
| {typography.body-lg} | 17px | 500 | 1.41 | 0 | Hero subheadline, emphasized paragraphs |
| {typography.body-md} | 17px | 500 | 1.41 | 0 | Standard body copy, form inputs |
| {typography.body-sm} | 15px | 400 | 1.47 | 0 | Secondary descriptive text |
| {typography.caption} | 14px | 500 | 1.43 | 0.02em | Metadata, footnotes, legal |
| {typography.button} | 15px | 700 | 1.33 | 0.05em | All button labels (uppercase) |
| {typography.label-uppercase} | 13px | 700 | 1.38 | 0.08em | Utility labels, nav items, language selector |
Principles
The Duolingo type system trusts weight over size to establish hierarchy. Notice that {typography.body-lg} and {typography.body-md} share identical 17px sizing — the distinction between a hero subheading and a paragraph comes entirely from context and surrounding whitespace, not from a size jump. Meanwhile, the gap between {typography.display-lg} at 32px and body at 17px creates a dramatic binary: you're either reading a "proclamation" or "content," with nothing in between. This reflects the app's gamified structure — clear signals for "level complete" versus "keep going."
All {typography.button} and {typography.label-uppercase} roles use uppercase transformation with positive letter-spacing, a treatment borrowed from video-game UI that makes calls-to-action feel like actionable commands rather than passive suggestions. The 0.05em–0.08em tracking prevents the cramped feeling that uppercase often produces in geometric fonts. Headlines avoid uppercase entirely, maintaining sentence-case for approachability.
Line heights are consistently tight (1.15–1.41 range), reflecting the relatively large x-height of DIN Round — the font doesn't need extra leading to remain readable. Letter-spacing is either zero or slightly negative (-0.01em) on display sizes, allowing the geometric forms to breathe naturally at scale.
Note on Font Substitutes
Both DIN Round and Feather are proprietary Duolingo fonts unavailable via Google Fonts. For implementation using open-source alternatives:
- Replace
din-roundwith Nunito (Google Fonts) — a rounded geometric sans with very similar proportions, x-height, and terminal treatment. Weight mapping: 400→Regular, 500→Medium, 700→Bold. - Replace
featherwith Quicksand (Google Fonts) — a friendly rounded display sans with similar warmth for the 48px headline role. - Define CSS variables:
--font-body: 'Nunito', 'din-round', sans-serif;and--font-display: 'Quicksand', 'feather', sans-serif;
If exact matching is not required, Montserrat (with rounded style settings) can serve as a single-font substitute for both roles across all weights.
Layout
Spacing System
- Base unit: 4px (inferred from token distribution).
- Tokens:
{spacing.xxs}(2px) — micro-adjustments, focus ring offsets{spacing.xs}(4px) — internal icon gaps, tight spacing{spacing.sm}(8px) — small element gaps, padding inside compact components{spacing.md}(16px) — standard internal padding, grid gutters{spacing.lg}(24px) — component-to-component gaps, card internal padding{spacing.xl}(48px) — major section internal spacing{spacing.section}(96px) — vertical band separation, used on{component.hero-band}and section wrappers{spacing.hero-top}(70px) — top padding offset for header area{spacing.button-pad-x}(16px) — horizontal padding for standard buttons{spacing.component-gap}(10px) — vertical gap between stacked CTA buttons
- Section padding (vertical):
{spacing.section}(96px) — applied to main content wrapper sections. - Card internal padding:
{spacing.lg}(24px) on{component.card-surface}components. - Gutters:
{spacing.md}(16px) horizontal padding within the max-width container.
Grid & Container
- Max content width: 1440px (extracted from main and section container widths).
- Nav constrained width: 988px for the
{component.top-nav}inner content area. - Density choice: Editorial-acquisition density — the hero uses a roughly 55/45 split between text column and illustration column, prioritizing message delivery over information density. No marketplace-style grids on this landing view.
- Desktop layout: Two-column hero with left illustration (~45% width via
{component.hero-illustration-container}) and right text column ({component.hero-text-column}at ~55%). Language carousel below uses horizontal scrolling/flex layout. - Hero column split: Illustration left, text right — the reverse of typical SaaS patterns, letting the character illustration lead visually before the user reads the value proposition.
Whitespace Philosophy
Duolingo embraces generous, confident whitespace. The 96px {spacing.section} vertical padding ensures the hero band feels like a "place" rather than a strip. Internal component spacing uses consistent 24px ({spacing.lg}) gaps between the headline, subheadline, and CTA stack — each element has room to be understood independently. This is not airy-editorial whitespace (like a fashion brand); it's instructional whitespace — the kind that reduces cognitive load for users who may be evaluating whether to commit to learning a new language.
Header Architecture
┌─────────────────────────────────────────────────────────────┐
│ [DUOLINGO logo] [SITE LANGUAGE ▾] │
│ (primary green) (muted, caps) │
└─────────────────────────────────────────────────────────────┘
←←←←←←←←←← max-width: 1440px →→→→→→→→→→→→→→→→→→→→→→→→→→→
The {component.top-nav} is minimal: flush-left logo in {colors.primary}, flush-right language selector in {colors.muted} uppercase. No navigation links, no user avatar, no search — this is an unauthenticated acquisition page stripped to essentials. Height is fluid (auto) with {spacing.hero-top} (70px) top padding pushing content down.
Hero Section
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ │ │
│ │ CHARACTER │ │ The free, fun, and effective │ │
│ │ ILLUSTRATION │ │ way to learn a language! │ │
│ │ CLUSTER │ │ │ │
│ │ (Duo + │ │ ┌───────────────────────────┐ │ │
│ │ learners) │ │ │ GET STARTED │ │ │
│ │ │ │ └───────────────────────────┘ │ │
│ │ │ │ ┌───────────────────────────┐ │ │
│ │ │ │ │ I ALREADY HAVE AN ACCOUNT │ │ │
│ │ │ │ └───────────────────────────┘ │ │
│ └──────────────────┘ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
The {component.hero-band} splits into illustration-left, text-right. The illustration area contains the signature Duo-the-owl mascot surrounded by diverse learner characters in a floating, dynamic composition with no bounding box. The text column contains {component.hero-headline} in {typography.display-lg}, followed by implied subcopy (though not visible in this screenshot), then the {component.cta-stack} containing stacked {component.button-primary} ("GET STARTED") and {component.button-secondary} ("I ALREADY HAVE AN ACCOUNT"). Vertical rhythm between headline and first button is approximately 40px.
Language Course Carousel
┌──────────────────────────────────────────────────────────────────────────┐
│ < [🇺🇸 ENGLISH] [🇪🇸 SPANISH] [🇫🇷 FRENCH] [🇩🇪 GERMAN] [...] > │
└──────────────────────────────────────────────────────────────────────────┘
The {component.language-carousel} sits at the viewport bottom with a horizontal scroll of {component.language-course-item} entries. Each item pairs a {component.language-course-flag-icon} (28px rounded rectangle with country flag) with a {component.language-course-label} in uppercase tracking. Chevron navigation arrows appear at the scroll boundaries. Background is {colors.surface-soft} separated by a thin rule.
Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| 0 — Flat | No shadow, no border | Base canvas, hero band, static text |
| 1 — Border-only | 1px solid {colors.border-default} | Form inputs ({component.input-text}) |
| 2 — Soft shadow | {extensions.shadows.md} | Potential dropdown menus (inferred) |
| 3 — Raised | {extensions.shadows.lg} | Modals, overlays (inferred) |
| Button inset | {extensions.shadows.button-inset} | Tactile press effect on {component.button-primary-active} |
Duolingo employs a nearly flat elevation model. The landing page shows zero drop shadows on any visible component — buttons, cards, and the nav all sit flush against {colors.canvas}. Depth is communicated through color contrast alone: {colors.primary} pops forward because it's the only saturated hue; {colors.muted} recedes because it's the lightest gray. The extracted CSS confirms boxShadow: "none" across all button classes.
When elevation does appear (inferred from app-wide patterns), shadows are cool-toned and extremely subtle — never the warm, diffuse shadows common in material systems. The button active state uses an inset shadow technique (inset 0 -3px 0 rgba(0,0,0,0.12)) rather than a color shift to simulate physical depression. This "tactile flat" approach means the UI feels pressable despite lacking traditional depth cues.
Decorative Depth
No gradients, no noise textures, no photographic depth layers on this landing page. The only decorative element is the character illustration cluster, which provides visual interest through subject matter rather than through atmospheric effects. If gradient overlays exist elsewhere in the Duolingo ecosystem (e.g., Super Duolingo upsell modals), they are absent from this canonical acquisition surface.
Shapes
Border Radius Scale
| Token | Value | Use |
|---|---|---|
| {rounded.sm} | 8px | Small elements, flag icons, compact chips |
| {rounded.DEFAULT} | 12px | Primary buttons, secondary buttons, course items, all interactive cards |
| {rounded.md} | 12px | Alias for default — consistency marker |
| {rounded.lg} | 16px | Large cards, panels, image containers |
| {rounded.xl} | 20px | Feature cards, hero image masks |
| {rounded.pill} | 9999px | Badge pills, status indicators |
| {rounded.full} | 9999px | Avatar circles, fully rounded elements |
The shape language is consistently rounded — there are virtually no sharp-cornered rectangles in the UI. The 12px default radius is the "standard Duolingo corner," appearing on every button and card. This creates a toy-like, approachable quality that aligns with the gamified positioning. Even the {component.input-text} form fields, while showing borderRadius: 0px in raw extraction, likely receive rounding in production via class composition or the 12px default is applied at the wrapper level. Flag icons in the language carousel use {rounded.sm} (8px) to feel slightly more contained than full-blown buttons.
Photography Geometry
This landing page uses no photography — all visual communication is vector-based illustration. The character artwork (Duo and learners) floats without containment shapes (no circular masks, no rounded-rect frames). If imagery were introduced in other surfaces (blog posts, testimonial headshots), the expected pattern would be {rounded.lg} (16px) for rectangular images and {rounded.full} for avatars, consistent with the rounded design language.
Components
No hover states documented. Hover behavior is unreliable to extract from a single screenshot. State variants live as separate entries in the
components:frontmatter (e.g.{component.button-primary-hover},{component.input-text-focus}); add hover styling at implementation time per your stack's conventions.
Buttons
{component.button-primary} — The core conversion button. Bright {colors.primary} green background with {colors.on-primary} dark text. Uses {typography.button} (15px, weight 700, uppercase, 0.05em letter-spacing). Rounded to {rounded.DEFAULT} (12px). Padding is 14px {spacing.button-pad-x} horizontally with a fixed 52px height ensuring comfortable touch targets. Features an inset box-shadow on active state ({extensions.shadows.button-inset}) creating a "press" effect. State variants: {component.button-primary-hover} (darkened to {colors.primary-hover}), {component.button-primary-active} (darkest green {colors.primary-active} plus inset shadow), and {component.button-primary-disabled} (desaturated {colors.primary-disabled}). Appears as "GET STARTED" in the hero CTA stack.
{component.button-secondary} — Ghost/outline-style alternative action. Transparent background with {colors.link} sky-blue text. Same {typography.button} uppercase treatment and {rounded DEFAULT} 12px radius as primary. On hover, receives {colors.surface-soft} background ({component.button-secondary-hover}). On active, shifts to {colors.hairline} background with {colors.link-active} deep navy text ({component.button-secondary-active}). Appears as "I ALREADY HAVE AN ACCOUNT" below the primary CTA.
{component.button-outline} — Bordered ghost variant with transparent background and {colors.ink} text. Same dimensions and typography as primary/secondary. Intended for tertiary actions where neither green nor blue semantic meaning applies. Likely includes a 1px solid {colors.border-default} border (not extractable from screenshot but inferred from naming convention).
{component.button-ghost} — Minimal utility button with transparent background, {colors.muted} light gray text, and {typography.label-uppercase} (13px, 700 weight, 0.08em tracking). Zero border-radius (sharp corners acceptable for this micro-action). Used for the "SITE LANGUAGE: ENGLISH" selector in the {component.top-nav}. Hover likely shifts text to {colors.ink}.
Cards & Containers
{component.card-surface} — Standard content card. {colors.canvas} background with {colors.ink} text. Rounded to {rounded.lg} (16px). Internal padding of {spacing.lg} (24px). No visible shadow or border in the flat design system — separation from background relies on subtle {colors.surface-soft} parent or intentional placement. Would wrap blog post previews, feature explanations, or testimonial blocks.
{component.card-surface-bordered} — Variant of {component.card-surface} with explicit border treatment. Same dimensions and padding, but includes 1px solid {colors.border-default} or {colors.hairline} outline for definition when placed on non-contrasting backgrounds.
{component.cta-stack} — Vertical stacking container for call-to-action buttons. Transparent background. Enforces {spacing.component-gap} (10px) vertical gap between child buttons. Contains the primary/secondary pair in the hero section.
{component.language-carousel} — Horizontal scrolling container for course options. Spans full viewport width. Uses {spacing.xl} (48px) vertical padding. Contains horizontally arranged {component.language-course-item} children with overflow scroll behavior. Navigation chevrons at edges (not separately spec'd as components but present in layout).
Inputs & Forms
{component.input-text} — Standard text input / textarea. {colors.canvas} background with {colors.ink} text color. {typography.body-md} (17px, weight 500). 1px solid {colors.border-default} (#c1c1c1) border. Extracted CSS shows borderRadius: 0px, though this may be overridden by design-system defaults to match the 12px convention. Padding of 12px {spacing.md} (16px) internally. Fixed 48px height for single-line inputs. Focus state ({component.input-text-focus}) would typically shift border to {colors.primary} or {colors.border-strong} with possible subtle glow.
Navigation
{component.top-nav} — Site header bar. {colors.canvas} background spanning full width. Contains {component.nav-logo} on the left and {component.language-selector-button} on the right. Vertical padding of {spacing.hero-top} (70px) top and {spacing.lg} (24px) bottom, creating substantial breathing room above the hero content. Inner content constrains to 988px max-width centered.
{component.nav-logo} — Duolingo wordmark. Renders in {colors.primary} green. Uses {typography.display-lg} sizing (32px) scaled to fit the wordmark proportions. Transparent background, clickable, routes to home.
{component.language-selector-button} — Dropdown trigger for site language selection. Ghost button style with {colors.muted} text. Uses {typography.label-uppercase} (13px, 700 weight, wide tracking). Includes chevron-down indicator (icon not separately specified). Rounded to {rounded.sm} (8px) with 8px×12px internal padding.
{component.footer-nav} — Bottom navigation band for language course selection. {colors.surface-soft} (#f7f7f7) background distinguishing it from the main canvas. {colors.muted} text color at {typography.caption} size. Horizontal layout with flag+label pairs.
Sub-Systems: Language Course Selection
{component.language-course-flag-icon} — Small flag representation for each language. 28×28px square with {rounded.sm} (8px) corners. Displays national flag graphic. Paired horizontally with label text.
{component.language-course-label} — Language name in uppercase. {typography.label-uppercase} styling (13px, 700 weight, 0.08em tracking). {colors.ink} text. 4px right-padding to separate from subsequent elements or flags.
{component.language-course-item} — Combined clickable unit of flag + label. Transparent background, {colors.ink} text, {rounded.sm} radius, {spacing.sm}/{{spacing.md} padding. Represents a tappable course option. Hover state ({component.language-course-item-hover}) fills background with {colors.surface-soft}.
Signature Components
{component.badge-pill} — Status or achievement badge. {colors.secondary} golden-yellow background with {colors.on-primary} dark text. Fully rounded via {rounded.pill} (9999px). Compact padding of 4px×12px. Uses {typography.label-uppercase}. Would appear next to usernames for Super Duolingo subscribers, streak counts, or achievement unlocks.
{component.divider-horizontal} — 1px horizontal rule line. {colors.hairline} color. Full-width or constrained. Used between the main hero content and the language carousel footer.
Do's and Don'ts
Do
- Do use
{colors.primary}(#58cc02) exclusively for primary conversion actions — it loses impact if spread to borders, icons, or decorative elements. - Do apply
{rounded.DEFAULT}(12px) to every button and tappable card; sharp corners break the toy-like design language. - Do set
{typography.button}text to uppercase with 0.05em+ letter-spacing — lowercase buttons look unfinished in this system. - Do maintain the 10px (
{spacing.component-gap}) vertical gap when stacking{component.button-primary}above{component.button-secondary}; this specific rhythm is part of the hero's visual DNA. - Do use
{colors.accent}sky-blue for all non-primary links and secondary CTAs; never substitute{colors.primary}green for link color. - Do keep
{typography.display-lg}headlines at sentence case — uppercase headings feel corporate and counter to the friendly voice. - Do allow generous
{spacing.section}(96px) vertical padding between major bands; compressing this makes the page feel rushed. - Do use
din-round(or Nunito substitute) at medium (500) weight for body copy; regular (400) weight feels too light for this brand's confident tone. - Do place character illustrations (Duo + learners) on the left side of the hero when using the two-column layout; this is the established visual flow for acquisition pages.
- Do render the
{component.language-selector-button}in{colors.muted}with{typography.label-uppercase}treatment — it should feel like a utility, not a feature.
Don't
- Don't add drop shadows to
{component.button-primary}or{component.card-surface}— the flat aesthetic is intentional and brand-defining; use the inset shadow only for{component.button-primary-active}pressed states. - Don't introduce new brand colors beyond the
{colors.primary}/{colors.accent}/{colors.secondary}trio — the palette discipline is what keeps the UI feeling coherent and game-like. - Don't use
{typography.display-xl}(48px feather) for anything other than the single hero headline — it's a signature moment, not a general-purpose heading size. - Don't apply
{rounded.full}(9999px) radius to buttons — reserve pill shapes for{component.badge-pill}status indicators only; buttons should always have{rounded DEFAULT}(12px). - Don't set form input borders thicker than 1px or in any color besides
{colors.border-default}(#c1c1c1) — heavy input chrome fights the lightweight UI aesthetic. - Don't use
{colors.link-active}(#042c60) navy for default link states — reserve it for visited/active states only; default links must be{colors.link}(#1cb0f6). - Don't reduce the
{component.button-primary}height below 52px — this size ensures WCAG AAA touch targets and matches the extracted specification. - Don't mix
featherfont into body text or UI labels — it's reserved for the{typography.display-xl}display role only; everything else usesdin-round. - Don't add gradient overlays or noise textures to
{component.hero-band}— the pure{colors.canvas}white background is a deliberate choice that keeps focus on the illustration and message.
Motion & Animation
Transition Tokens
--transition-border-color: border-color 0.3s ease;
--transform-bounce: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
--filter-fade: filter 0.2s ease;
--transform-smooth: transform 0.5s ease-in-out;
--color-standard: color 0.2s ease, background-color 0.2s ease;
The extracted CSS reveals a bounce-forward easing curve (cubic-bezier(0.22, 1, 0.36, 1)) used for transforms — this is a custom "overshoot" curve that produces a slight springy quality when elements move or scale. It's characteristic of playful/gamified interfaces where motion should feel responsive rather than purely functional. The 0.4s duration on this transition gives enough time for the bounce to be perceptible without feeling sluggish.
Standard color transitions use shorter 0.2s durations with linear/ease timing, keeping color shifts snappy. Filter transitions (likely opacity or blur effects) sit at 0.2s. A slower 0.5s ease-in-out transform exists for larger spatial movements like panel slides or scroll-parallax.
Keyframe Animations
- onetrust-fade-in — Cookie consent overlay fade-in (third-party script, not brand animation). Not representative of the design system's own motion language.
Interaction Patterns
- Button press response: Primary buttons use the inset shadow technique (
{extensions.shadows.button-inset}) combined with{colors.primary-active}color shift, creating a 3D "depression" effect on tap/click. - Language carousel scroll: Horizontal smooth-scroll with momentum. Course items likely scale subtly (1.0→1.02) on hover via
--transform-bounce. - Focus rings: Form inputs probably gain a
{colors.primary}green 2px outline or glow on focus (inferred from brand color application patterns). - Language selector dropdown: Chevron rotation (180° flip) via
--transform-smoothtiming. Dropdown panel slides down with slight Y-translate. - Character illustration idle animation: Duo and learners likely have subtle floating/bobbing keyframe animations (2-4s cycle, translate-Y ±4px) — this is standard for Duolingo's character presentation but not captured in static CSS extraction.
Note: Many animations in the Duolingo ecosystem are JS-driven (Framer Motion or GSAP) rather than pure CSS transitions, especially for the character illustrations and celebration/confetti sequences. The CSS transitions extracted here represent the baseline layer; richer motion is layered programmatically.
Imagery Style
- Primary medium: Custom vector illustration — flat, colorful, character-driven artwork created in-house or by specialized illustrators.
- Characters: Duo the green owl mascot (brand icon) surrounded by diverse human learner avatars with varied skin tones, hair colors, and clothing styles — representing inclusivity and global reach.
- Art style: Flat vector with minimal shading (cell-shaded or unshaded). Bold outlines on some elements, clean fills. Characters have large eyes and exaggerated expressions for emotional clarity.
- Color palette (illustration): Extends the UI palette —
{colors.primary}green for Duo, plus vibrant supplementary hues (coral, cyan, purple, orange) for learner clothing and accessories. Never competes with UI chrome. - Composition: Dynamic, floating clusters without ground planes or perspective backgrounds. Characters overlap casually, creating depth through layering rather than environmental detail.
- Role in layout: Illustration is a co-equal partner to text in the hero — roughly 45% of horizontal space. It's not a background texture; it's a focal point that draws the eye before the headline registers.
- No photography: Zero photographic imagery on this surface. If photos appear elsewhere (blog, testimonials), they would likely be treated with
{rounded.lg}corners and potentially subtle duotone tinting toward{colors.primary}or monochrome. - Iconography within illustrations: Props (smartphones, books, speech bubbles) use simplified iconic forms consistent with the flat style.
Icon System
- Library: Custom SVG icon set (no third-party library detected in framework analysis).
- Specific icons observed:
- Chevron-down (language selector dropdown arrow)
- Chevron-left/right (carousel navigation arrows)
- Duolingo owl logo mark (wordmark companion)
- Country flag graphics (treated as image assets, not icon glyphs)
- Treatment: Icons appear to be filled (not stroked) vector shapes. Color is typically
currentColorinherited from parent text color, or explicitly set to{colors.muted}for utility icons and{colors.primary}for brand-affiliated marks. - Size: Utility icons (chevrons) estimate at 16–18px. Logo mark scales proportionally to wordmark (approximately 32–40px height). Flag icons in language carousel are 28×28px bounded by
{component.language-course-flag-icon}. - Alignment: Icons center vertically with adjacent text. In the language selector, chevron-down sits to the right of label text with ~4px gap. In carousel navigation, chevrons are vertically centered to the course item row.
Recommended Frontend Stack
- Framework: Next.js (React) / Astro (if static) — Duolingo uses a modern React stack
- Styling: CSS Modules or Styled Components with design-token consumption layer
- Fonts: Self-hosted woff2 files for din-round and feather; fall back to Nunito/Quicksand from Google Fonts CDN
- Animation: Framer Motion (React) for character animations; vanilla CSS transitions for UI state
- Icons: Custom SVG sprite / inline SVG components (no icon library dependency)
- Component lib: Custom component library (no shadcn/Radix detected — Duolingo builds its own primitives)
Responsive Behavior
Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 640px | Single-column hero; illustration above text or hidden; full-width buttons; carousel becomes vertically scrollable list |
| Tablet | 640–1024px | Two-column hero maintained but with tighter ratios (50/50); reduced {spacing.section} padding to 64px |
| Desktop | 1025–1440px | Canonical layout as documented; 1440px max container width |
| Wide | > 1440px | Container centers with auto margins; additional horizontal padding may increase |
Touch Targets
{component.button-primary}: 52px height × variable width (minimum ~140px with padding) — exceeds WCAG AAA 44×44 minimum comfortably.{component.button-secondary}: 52px height × variable width — same comfortable touch compliance.{component.button-ghost}(language selector): Approximately 36px height × auto-width — may fall short of 44px recommendation; consider increasing padding to10px 16pxfor mobile.{component.language-course-item}: Approximately 44px height (flag 28px + padding) — meets minimum; ensure tap area extends to full row width.
Collapsing Strategy
- Navigation:
{component.top-nav}collapses logo and language selector onto same horizontal plane; no hamburger menu needed at this minimal link count. At mobile, language selector may move below logo. - Hero columns: Two-column layout (illustration left, text right) stacks vertically on mobile. Illustration moves above headline (visual priority) or is reduced in scale. Text column becomes full-width with centered alignment.
- CTA stack:
{component.button-primary}and{component.button-secondary}remain stacked vertically but expand to full available width (padding increases to14px 32px). - Language carousel: Horizontal scroll converts to vertical scrollable list or a 2-column grid of course items. Chevron navigation hides; native scroll replaces it.
- Spacing compression:
{spacing.section}reduces from 96px to ~56–64px on mobile.{spacing.lg}(24px) card padding may reduce to 16px.
Image Behavior
- Hero illustration: Scales down proportionally on mobile, likely capped at 280–320px max-width. May switch to a simplified "Duo-only" composition if full character cluster is too detailed for small screens.
- Flag icons: Maintain 28px size or reduce slightly to 24px on mobile. Never scale below 20px (unrecognizable).
- Logo: Wordmark scales responsively; owl icon mark may hide on very small screens leaving text-only logo.
Iteration Guide
-
Build a landing page that closely mimics the design system in this DESIGN.md. Use Next.js with CSS Modules (or Tailwind with custom design-token extension) for the styling stack — Duolingo's actual stack is React-based with strong token governance.
-
Pull every color, font, radius, and spacing value from the YAML frontmatter via the
{section.token}paths. Never hard-code a value that has a token. Create a JavaScript/TypeScript tokens object or CSS custom properties file that maps exactly to the frontmatter keys. -
Add state variants (
-hover,-active,-disabled) per your stack's conventions. The spec documents Default and Active/Pressed/Disabled explicitly; implement:hoverand:focus-visiblepseudo-classes using the corresponding component entries (e.g.,{component.button-primary-hover}styles apply on:hoverof{component.button-primary}). -
When adding a new component, decide first which sub-system it belongs to: the current page shows primarily the Acquisition/Onboarding dialect (hero, CTA, language selection). Other dialects (Dashboard, Learning Exercise, Settings) may have different surface rules but should share these core tokens.
-
Variants of an existing component live as separate frontmatter entries (
button-primary-active, never nested underbutton-primary.states.active). This flat structure maps cleanly to CSS class names or styled-component variants. -
Run
npx @google/design.md lint DESIGN.mdafter edits —broken-ref,contrast-ratio, andorphaned-tokenswarnings flag issues automatically. Pay special attention to{colors.on-primary}contrast ratio against{colors.primary}(current combo passes AA for large text). -
When in doubt about emphasis: bigger type before bolder type (the system already uses 700 weight generously), signature illustration placement (left-side hero art) before solid accent bars, and rounded corners (12px default) before sharp edges.
-
For the dual-font system, define CSS custom properties early:
:root { --font-body: 'din-round', 'Nunito', sans-serif; --font-display: 'feather', 'Quicksand', sans-serif; }Apply
--font-bodyto everything except.display-xlwhich gets--font-display. -
Implement the inset shadow button press effect as the primary active-state affordance — it's the most distinctive interaction pattern in this design system and separates Duolingo from generic SaaS UI.
-
Test the
{component.language-selector-button}and{component.language-course-item}components at 320px viewport width to verify touch targets meet 44px minimum. Adjust padding values upward if needed, referencing{spacing.md}and{spacing.lg}tokens.
Font Setup
Since DIN Round and Feather are proprietary Duolingo fonts, substitute as follows for implementation:
- DIN Round → Nunito (weights: 400 Regular, 500 Medium, 700 Bold). Load from Google Fonts:
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;700&display=swap" rel="stylesheet"> - Feather → Quicksand (weight: 700 Bold for display use). Load from Google Fonts:
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@700&display=swap" rel="stylesheet">
Define CSS variables:
:root {
--font-sans: 'Nunito', 'din-round', -apple-system, BlinkMacSystemFont, sans-serif;
--font-display: 'Quicksand', 'feather', 'Nunito', sans-serif;
}
Apply font-family: var(--font-sans) globally, override to var(--font-display) for the .display-xl or .hero-title class only.
Known Gaps
- Hover state visual specs are inferred from naming conventions and the extracted transition CSS, but exact color/transform values for
:hoverpseudo-classes on most components are not directly observable from the static screenshot. - Loading/skeleton states are not visible on this pre-rendered landing page — the app likely uses pulsing skeleton placeholders or Duo-themed loading animations not captured here.
- Form validation states (error, success feedback on inputs) are referenced semantically (
{colors.error},{colors.success}) but no red-bordered or check-marked input examples were present to document their exact rendering. - Dropdown/overlay panels (language selector expanded state, potential account menu) are closed in the screenshot; their internal layout, shadow treatment, and z-index stacking are undocumented.
- Dark mode token set is not defined — Duolingo does offer dark theme in-app, but this landing page is light-mode-only. All dark-surface tokens (
{colors.on-dark}, etc.) are placeholders. - Responsive breakpoint-specific adjustments to the hero illustration (simplified composition, repositioned elements) cannot be determined from a single desktop viewport capture.
- Streak counter / gamification widgets (fire icons, daily goal progress rings) are signature Duolingo UI elements absent from this acquisition page — their component specifications would require dashboard/app screenshots.
- Accessibility annotations (ARIA labels, focus management order, screen-reader-only text) are not extractable from visual/CSS data alone.
- CSS variable naming mismatches in source code: Duolingo's internal tokens may use different naming conventions (e.g.,
--color-brand-greenvs. our{colors.primary}) — this spec normalizes to the design.md schema but implementation will need a mapping layer.