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.236Implementation
// 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 heightParagraph 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 minimumContrast
Normal text: 4.5:1 contrast minimum
Large text (18px+ or 14px+ bold): 3:1 minimumRespect 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
- Consistent Scale: Use type scale, not arbitrary sizes
- Semantic Tokens: Map meanings (h1, body) to scale values
- Mobile-First: Start small, scale up
- Line Length: 45-75 characters optimal
- Line Height: Larger for smaller text
- Font Loading:
font-display: swapto prevent invisible text - Variable Fonts: Consider for performance and flexibility
- Accessibility: 16px minimum, respect user preferences
- Contrast: Test all text/background combinations
- 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!