Front-end Engineering Lab
PatternsDesign Systems

Typography System

Build a scalable, accessible type scale and hierarchy

Typography System

Typography is the foundation of your UI. A good typography system creates visual hierarchy, improves readability, and establishes your brand identity.

Type Scale

A type scale is a set of harmonious font sizes based on a ratio.

Common Ratios

Minor Third (1.2):     1.000 → 1.200 → 1.440 → 1.728
Major Third (1.25):    1.000 → 1.250 → 1.563 → 1.953
Perfect Fourth (1.333): 1.000 → 1.333 → 1.777 → 2.369
Golden Ratio (1.618):  1.000 → 1.618 → 2.618 → 4.236

Implementation

// tokens/typography.ts
export const typography = {
  fontSize: {
    xs: '0.75rem',     // 12px
    sm: '0.875rem',    // 14px
    base: '1rem',      // 16px (1.000)
    lg: '1.125rem',    // 18px (1.125)
    xl: '1.25rem',     // 20px (1.250)
    '2xl': '1.5rem',   // 24px (1.500)
    '3xl': '1.875rem', // 30px (1.875)
    '4xl': '2.25rem',  // 36px (2.250)
    '5xl': '3rem',     // 48px (3.000)
    '6xl': '3.75rem',  // 60px (3.750)
    '7xl': '4.5rem',   // 72px (4.500)
  },
  
  fontWeight: {
    normal: '400',
    medium: '500',
    semibold: '600',
    bold: '700',
    extrabold: '800',
  },
  
  lineHeight: {
    none: '1',
    tight: '1.25',
    snug: '1.375',
    normal: '1.5',
    relaxed: '1.625',
    loose: '2',
  },
  
  letterSpacing: {
    tighter: '-0.05em',
    tight: '-0.025em',
    normal: '0',
    wide: '0.025em',
    wider: '0.05em',
    widest: '0.1em',
  },
  
  fontFamily: {
    sans: [
      'Inter',
      '-apple-system',
      'BlinkMacSystemFont',
      'Segoe UI',
      'Roboto',
      'sans-serif',
    ].join(', '),
    
    serif: [
      'Georgia',
      'Cambria',
      'Times New Roman',
      'serif',
    ].join(', '),
    
    mono: [
      'JetBrains Mono',
      'Menlo',
      'Monaco',
      'Consolas',
      'monospace',
    ].join(', '),
  },
};

Semantic Typography Tokens

// Map semantic names to scale values
export const semanticTypography = {
  // Body text
  body: {
    fontSize: typography.fontSize.base,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.normal,
  },
  
  bodySmall: {
    fontSize: typography.fontSize.sm,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.normal,
  },
  
  // Headings
  h1: {
    fontSize: typography.fontSize['5xl'],
    lineHeight: typography.lineHeight.tight,
    fontWeight: typography.fontWeight.bold,
    letterSpacing: typography.letterSpacing.tight,
  },
  
  h2: {
    fontSize: typography.fontSize['4xl'],
    lineHeight: typography.lineHeight.tight,
    fontWeight: typography.fontWeight.bold,
    letterSpacing: typography.letterSpacing.tight,
  },
  
  h3: {
    fontSize: typography.fontSize['3xl'],
    lineHeight: typography.lineHeight.snug,
    fontWeight: typography.fontWeight.semibold,
  },
  
  h4: {
    fontSize: typography.fontSize['2xl'],
    lineHeight: typography.lineHeight.snug,
    fontWeight: typography.fontWeight.semibold,
  },
  
  h5: {
    fontSize: typography.fontSize.xl,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.semibold,
  },
  
  h6: {
    fontSize: typography.fontSize.lg,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.semibold,
  },
  
  // UI elements
  button: {
    fontSize: typography.fontSize.sm,
    lineHeight: typography.lineHeight.none,
    fontWeight: typography.fontWeight.medium,
    letterSpacing: typography.letterSpacing.wide,
  },
  
  caption: {
    fontSize: typography.fontSize.xs,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.normal,
  },
  
  overline: {
    fontSize: typography.fontSize.xs,
    lineHeight: typography.lineHeight.normal,
    fontWeight: typography.fontWeight.semibold,
    letterSpacing: typography.letterSpacing.widest,
    textTransform: 'uppercase',
  },
  
  code: {
    fontSize: typography.fontSize.sm,
    lineHeight: typography.lineHeight.normal,
    fontFamily: typography.fontFamily.mono,
  },
};

CSS Implementation

/* global.css */
:root {
  /* Font families */
  --font-sans: Inter, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  
  /* Font sizes */
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 1.875rem;
  --font-size-4xl: 2.25rem;
  --font-size-5xl: 3rem;
  
  /* Line heights */
  --line-height-tight: 1.25;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.625;
  
  /* Font weights */
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;
}

/* Base styles */
body {
  font-family: var(--font-sans);
  font-size: var(--font-size-base);
  line-height: var(--line-height-normal);
  font-weight: var(--font-weight-normal);
}

/* Heading styles */
h1 {
  font-size: var(--font-size-5xl);
  line-height: var(--line-height-tight);
  font-weight: var(--font-weight-bold);
}

h2 {
  font-size: var(--font-size-4xl);
  line-height: var(--line-height-tight);
  font-weight: var(--font-weight-bold);
}

h3 {
  font-size: var(--font-size-3xl);
  line-height: var(--line-height-tight);
  font-weight: var(--font-weight-semibold);
}

/* Code */
code, pre {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

React Components

// components/Text.tsx
import { semanticTypography } from '@/tokens/typography';

type TextVariant = keyof typeof semanticTypography;

interface TextProps {
  variant?: TextVariant;
  as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
  children: React.ReactNode;
}

export function Text({ variant = 'body', as: Component = 'p', children }: TextProps) {
  const styles = semanticTypography[variant];
  
  return (
    <Component style={styles}>
      {children}
    </Component>
  );
}

// Usage
<Text variant="h1" as="h1">Heading</Text>
<Text variant="body">Body text</Text>
<Text variant="caption">Small caption</Text>

Responsive Typography

/* Mobile-first responsive headings */
h1 {
  font-size: 1.875rem; /* 30px */
}

@media (min-width: 768px) {
  h1 {
    font-size: 2.25rem; /* 36px */
  }
}

@media (min-width: 1024px) {
  h1 {
    font-size: 3rem; /* 48px */
  }
}

/* Or fluid typography */
h1 {
  font-size: clamp(1.875rem, 5vw, 3rem);
}

Readability Guidelines

Line Length

/* Optimal: 45-75 characters per line */
.prose {
  max-width: 65ch;  /* 65 characters */
}

.prose-narrow {
  max-width: 45ch;
}

.prose-wide {
  max-width: 75ch;
}

Line Height

Body text (16px): 1.5 (24px)
Large text (20px): 1.4 (28px)
Headings (36px): 1.2 (43px)

Rule of thumb: Smaller text = larger line height

Paragraph Spacing

p {
  margin-bottom: 1.5rem;
}

p:last-child {
  margin-bottom: 0;
}

Font Loading Strategy

1. System Fonts (Fastest)

body {
  font-family: 
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    'Roboto',
    sans-serif;
}

Pros: Instant, no download
Cons: Less brand control

2. Self-Hosted Fonts

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
}

Pros: Full control, privacy
Cons: Must host and optimize

3. Google Fonts

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
});

export default function RootLayout({ children }) {
  return (
    <html className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

Pros: Easy, optimized by Google
Cons: External dependency

Font Display Strategy

@font-face {
  font-display: swap;
  /* 
    swap: Show fallback immediately, swap when font loads
    optional: Use font only if cached, else fallback
    fallback: Show fallback for ~100ms, swap if font loads quickly
  */
}

Variable Fonts

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;  /* Single file, all weights */
  font-style: oblique 0deg 10deg;  /* Variable slant */
}

/* Use any weight */
h1 {
  font-weight: 650;  /* Exact weight between 600 and 700 */
}

Benefits: Single file, any weight, smooth animations

Accessibility

Minimum Font Sizes

Body text: 16px minimum (18px recommended)
Small text: 14px minimum
Legal text: 12px absolute minimum

Contrast

Normal text: 4.5:1 contrast minimum
Large text (18px+ or 14px+ bold): 3:1 minimum

Respect User Preferences

/* Respect user's font size preference */
html {
  font-size: 100%;  /* User's browser default (usually 16px) */
}

/* Use rem for sizing */
h1 {
  font-size: 3rem;  /* Scales with user preference */
}

Testing Typography

// Visual regression test
describe('Typography', () => {
  it('matches snapshot', () => {
    const { container } = render(
      <div>
        <Text variant="h1">Heading 1</Text>
        <Text variant="h2">Heading 2</Text>
        <Text variant="body">Body text</Text>
      </div>
    );
    
    expect(container).toMatchSnapshot();
  });
});

Best Practices

  1. Consistent Scale: Use type scale, not arbitrary sizes
  2. Semantic Tokens: Map meanings (h1, body) to scale values
  3. Mobile-First: Start small, scale up
  4. Line Length: 45-75 characters optimal
  5. Line Height: Larger for smaller text
  6. Font Loading: font-display: swap to prevent invisible text
  7. Variable Fonts: Consider for performance and flexibility
  8. Accessibility: 16px minimum, respect user preferences
  9. Contrast: Test all text/background combinations
  10. Performance: Subset fonts, preload critical fonts

Common Pitfalls

Random sizes: 17px, 19px, 23px
Use type scale

Too many weights: 6 font weights loaded
3-4 weights maximum

Tiny mobile text: 12px body
16px minimum

Long lines: 120 characters
45-75 characters (65ch)

Tight line height: 1.0 on body text
1.5 minimum for body

A solid typography system is the backbone of great UI—invest time in getting it right!

On this page