CSS Typography
Typography is the foundation of web design. Studies show that 95% of web content is text, making typographic choices critical for readability, accessibility, and brand identity. CSS provides a rich set of properties for controlling type on the web, from basic font selection to advanced OpenType features.
A font stack (or font family stack) is a prioritized list of fonts. The browser tries each font in order until it finds one available on the system. Always end a font stack with a generic family keyword to ensure the browser has a fallback.
| 1 | body { |
| 2 | font-family: "Inter", "SF Pro", system-ui, -apple-system, sans-serif; |
| 3 | font-size: 1rem; |
| 4 | line-height: 1.6; |
| 5 | color: #E0E0E0; |
| 6 | } |
The font-family property specifies a prioritized list of font family names and generic family names. The browser selects the first available font from the list. Generic families ensure the page is readable even when custom fonts fail to load.
Generic Families
| Generic Family | Description | Example (Mac) |
|---|---|---|
| serif | Letters with small strokes (serifs) at the end of characters | Times New Roman, Georgia, Merriweather |
| sans-serif | Letters without serifs — clean and modern | Helvetica, Arial, Inter, SF Pro |
| monospace | All characters have the same width | SF Mono, Menlo, Consolas, Fira Code |
| cursive | Handwriting style, joined strokes | Brush Script, Pacifico, Comic Sans |
| fantasy | Decorative, ornamental characters | Papyrus, Impact, Joker |
Font Stacks
| 1 | /* System font stack — fastest performance, matches OS */ |
| 2 | body { |
| 3 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; |
| 4 | } |
| 5 | |
| 6 | /* Modern sans-serif stack */ |
| 7 | .sans { |
| 8 | font-family: "Inter", "SF Pro", "Helvetica Neue", Arial, sans-serif; |
| 9 | } |
| 10 | |
| 11 | /* Premium serif for body text */ |
| 12 | .serif { |
| 13 | font-family: "Merriweather", "Georgia", "Times New Roman", serif; |
| 14 | } |
| 15 | |
| 16 | /* Code font stack */ |
| 17 | code { |
| 18 | font-family: "Fira Code", "SF Mono", Menlo, "Cascadia Code", monospace; |
| 19 | } |
| 20 | |
| 21 | /* system-ui — uses the OS native UI font */ |
| 22 | .native { font-family: system-ui; } |
| 23 | /* -apple-system — macOS/iOS San Francisco */ |
| 24 | .apple { font-family: -apple-system; } |
The @font-face at-rule allows you to load custom fonts hosted on your server or via a CDN. Modern web font formats include WOFF2 (best compression), WOFF, and TTF.
Basic @font-face Declaration
| 1 | @font-face { |
| 2 | font-family: "Geist"; |
| 3 | src: url("/fonts/geist.woff2") format("woff2"), |
| 4 | url("/fonts/geist.woff") format("woff"); |
| 5 | font-weight: 400; |
| 6 | font-style: normal; |
| 7 | font-display: swap; |
| 8 | unicode-range: U+0000-00FF; |
| 9 | } |
| 10 | |
| 11 | @font-face { |
| 12 | font-family: "Geist"; |
| 13 | src: url("/fonts/geist-bold.woff2") format("woff2"); |
| 14 | font-weight: 700; |
| 15 | font-style: normal; |
| 16 | font-display: swap; |
| 17 | } |
font-display Strategies
| Value | Behavior | Use Case |
|---|---|---|
| auto | Browser default — usually a short block period | Default, unknown performance |
| block | Invisible text for ~3s (block period), then swap | FOIT — icon fonts, brand typography |
| swap | Fallback font shown immediately, swap when loaded | Best UX — text visible immediately |
| fallback | Short block (~100ms), then swap | Balance between FOIT and FOUT |
| optional | Extremely short block, may not swap if network is slow | Non-critical typography |
unicode-range
unicode-range limits a font-face to specific character ranges, enabling you to load smaller font files for Latin text while using a different font for CJK characters, emoji, or special symbols.
| 1 | /* Load a smaller font for basic Latin (A-Z, 0-9) */ |
| 2 | @font-face { |
| 3 | font-family: "MyFont"; |
| 4 | src: url("myfont-latin.woff2") format("woff2"); |
| 5 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC; |
| 6 | } |
| 7 | |
| 8 | /* A different font for emoji */ |
| 9 | @font-face { |
| 10 | font-family: "MyFont"; |
| 11 | src: url("myfont-emoji.woff2") format("woff2"); |
| 12 | unicode-range: U+1F600-1F64F; |
| 13 | } |
pro tip
Font size can be specified using absolute units (px, pt, pc), relative units (em, rem, %), viewport units (vw, vh), or the CSS math functions clamp(), min(), and max().
Absolute vs Relative
| 1 | .absolute { |
| 2 | font-size: 16px; /* fixed size — ignores user preferences */ |
| 3 | } |
| 4 | |
| 5 | .relative-em { |
| 6 | font-size: 1.25em; /* relative to parent font-size */ |
| 7 | } |
| 8 | |
| 9 | .relative-rem { |
| 10 | font-size: 1.125rem; /* relative to root font-size (default 16px) */ |
| 11 | } |
| 12 | |
| 13 | .relative-percent { |
| 14 | font-size: 110%; /* 110% of parent */ |
| 15 | } |
| 16 | |
| 17 | .viewport { |
| 18 | font-size: 4vw; /* 4% of viewport width — scales with window */ |
| 19 | } |
Fluid Typography with clamp()
The clamp(MIN, PREFERRED, MAX) function creates fluid type that scales smoothly between a minimum and maximum size. This eliminates the need for multiple breakpoints for font sizes.
| 1 | /* Fluid heading — scales between 2rem and 4rem */ |
| 2 | h1 { |
| 3 | font-size: clamp(2rem, 1rem + 3vw, 4rem); |
| 4 | } |
| 5 | |
| 6 | /* Fluid body text — subtle scaling */ |
| 7 | p { |
| 8 | font-size: clamp(0.875rem, 0.8rem + 0.5vw, 1.125rem); |
| 9 | } |
| 10 | |
| 11 | /* Modular scale approach */ |
| 12 | :root { |
| 13 | --step-0: clamp(0.875rem, 0.8rem + 0.5vw, 1rem); |
| 14 | --step-1: clamp(1rem, 0.9rem + 0.8vw, 1.25rem); |
| 15 | --step-2: clamp(1.25rem, 1.1rem + 1.2vw, 1.75rem); |
| 16 | --step-3: clamp(1.75rem, 1.4rem + 2vw, 2.75rem); |
| 17 | --step-4: clamp(2.5rem, 1.8rem + 3vw, 4rem); |
| 18 | } |
Font weight controls the thickness of characters. Keywords and numeric values are interchangeable, with 400 typically mapping to normal and 700 to bold. Variable fonts enable continuous weight adjustment between 1 and 1000.
| 1 | .thin { font-weight: 100; } /* Thin / Hairline */ |
| 2 | .light { font-weight: 300; } /* Light */ |
| 3 | .normal { font-weight: 400; } /* Normal / Regular */ |
| 4 | .medium { font-weight: 500; } /* Medium */ |
| 5 | .semibold { font-weight: 600; } /* Semi Bold */ |
| 6 | .bold { font-weight: 700; } /* Bold */ |
| 7 | .extrabold { font-weight: 800; } /* Extra Bold */ |
| 8 | .black { font-weight: 900; } /* Black / Heavy */ |
| 9 | |
| 10 | /* Keyword equivalents */ |
| 11 | .normal { font-weight: normal; } /* = 400 */ |
| 12 | .bold { font-weight: bold; } /* = 700 */ |
| 13 | |
| 14 | /* Variable fonts — any weight between 1 and 1000 */ |
| 15 | .variable { |
| 16 | font-family: "Inter", sans-serif; |
| 17 | font-weight: 450; /* custom weight between Regular and Medium */ |
| 18 | font-variation-settings: "wght" 450; |
| 19 | } |
font-style controls italicization and obliqueness. font-variant enables OpenType features like small-caps, ligatures, and numeric variants.
| 1 | /* font-style */ |
| 2 | .italic { font-style: italic; } /* uses the italic variant of the font */ |
| 3 | .oblique { font-style: oblique; } /* artificially slanted */ |
| 4 | .oblique { font-style: oblique 10deg; } /* oblique with angle */ |
| 5 | .normal { font-style: normal; } |
| 6 | |
| 7 | /* font-variant */ |
| 8 | .small-caps { font-variant: small-caps; } |
| 9 | |
| 10 | /* font-variant sub-properties */ |
| 11 | .contextual-ligatures { |
| 12 | font-variant-ligatures: common-ligatures contextual; |
| 13 | } |
| 14 | .tabular-nums { |
| 15 | font-variant-numeric: tabular-nums; |
| 16 | } |
| 17 | .proportional { |
| 18 | font-variant-numeric: proportional-nums; |
| 19 | } |
| 20 | .east-asian { |
| 21 | font-variant-east-asian: jis78; |
| 22 | } |
| 23 | |
| 24 | /* Shorthand */ |
| 25 | .all-small { font-variant: all-small-caps; } |
| 26 | } |
Line height controls the vertical space between lines of text. Unitless values are recommended because they are relative to the font-size and work correctly with inheritance. A line height of 1.5 to 1.7 is considered optimal for body text readability.
| 1 | /* Unitless (recommended) — relative to font-size */ |
| 2 | .tight { line-height: 1.2; } /* headings */ |
| 3 | .normal { line-height: 1.5; } /* default */ |
| 4 | .roomy { line-height: 1.7; } /* body text */ |
| 5 | .spacious { line-height: 2; } /* extended readability */ |
| 6 | |
| 7 | /* Length values — not recommended */ |
| 8 | .pixels { line-height: 24px; } /* fixed — does not scale with font-size */ |
| 9 | .em-unit { line-height: 1.5em; } /* em-based — can produce unexpected results */ |
| 10 | |
| 11 | /* Normal — browser default (~1.2) */ |
| 12 | .default { line-height: normal; } |
| 13 | |
| 14 | /* Leading tip for headings */ |
| 15 | h1, h2, h3 { |
| 16 | line-height: 1.15; /* tighter for larger text */ |
| 17 | } |
| 18 | |
| 19 | p { |
| 20 | line-height: 1.65; /* looser for body text readability */ |
| 21 | } |
letter-spacing (tracking) controls the horizontal space between characters. word-spacing controls the space between words. Small adjustments can significantly impact readability and tone.
| 1 | /* letter-spacing (tracking) */ |
| 2 | .condensed { letter-spacing: -0.02em; } /* tighter — use sparingly */ |
| 3 | .normal { letter-spacing: normal; } /* default (0) */ |
| 4 | .expanded { letter-spacing: 0.05em; } /* more open */ |
| 5 | .heading { letter-spacing: -0.03em; } /* popular for modern headings */ |
| 6 | .uppercase { letter-spacing: 0.08em; } /* common for all-caps text */ |
| 7 | |
| 8 | /* word-spacing */ |
| 9 | .normal { word-spacing: normal; } /* default (0) */ |
| 10 | .expanded-words { word-spacing: 0.25em; } /* extra space between words */ |
| 11 | |
| 12 | /* Combined */ |
| 13 | .meta { |
| 14 | text-transform: uppercase; |
| 15 | font-size: 0.75rem; |
| 16 | letter-spacing: 0.1em; /* improves uppercase readability */ |
| 17 | word-spacing: 0.15em; |
| 18 | } |
text-align controls horizontal alignment of inline content within its block container. text-indent specifies the indentation of the first line of a block of text.
| 1 | /* text-align */ |
| 2 | .left { text-align: left; } /* default for LTR languages */ |
| 3 | .right { text-align: right; } |
| 4 | .center { text-align: center; } |
| 5 | .justify { text-align: justify; } /* stretches lines to fill width */ |
| 6 | |
| 7 | /* text-indent */ |
| 8 | .indent { |
| 9 | text-indent: 2em; /* first line indented by 2 character widths */ |
| 10 | } |
| 11 | |
| 12 | .hanging { |
| 13 | text-indent: -2em; /* hanging indent — first line extends left */ |
| 14 | padding-left: 2em; /* needed with negative text-indent */ |
| 15 | } |
| 16 | |
| 17 | /* Combine with text-align */ |
| 18 | article p { |
| 19 | text-align: justify; |
| 20 | text-indent: 1.5em; |
| 21 | } |
| 22 | |
| 23 | /* No indent on first paragraph after a heading */ |
| 24 | h2 + p, h3 + p { |
| 25 | text-indent: 0; |
| 26 | } |
The text-decoration shorthand and its sub-properties give fine control over decorative lines on text. Modern CSS supports underline offset, thickness, and skip-ink behavior.
| 1 | /* text-decoration shorthand */ |
| 2 | .underline { |
| 3 | text-decoration: underline; |
| 4 | } |
| 5 | |
| 6 | .overline { |
| 7 | text-decoration: overline; |
| 8 | } |
| 9 | |
| 10 | .line-through { |
| 11 | text-decoration: line-through; |
| 12 | } |
| 13 | |
| 14 | .none { |
| 15 | text-decoration: none; /* remove decoration (common on links) */ |
| 16 | } |
| 17 | |
| 18 | /* Sub-properties for fine control */ |
| 19 | .fancy-underline { |
| 20 | text-decoration-line: underline; |
| 21 | text-decoration-style: wavy; /* solid | double | dotted | dashed | wavy */ |
| 22 | text-decoration-color: #00FF41; /* custom color */ |
| 23 | text-decoration-thickness: 2px; /* line thickness */ |
| 24 | text-underline-offset: 4px; /* space between text and line */ |
| 25 | text-decoration-skip-ink: auto; /* skips descenders for cleaner look */ |
| 26 | } |
| 27 | |
| 28 | /* Multiple lines */ |
| 29 | .both { |
| 30 | text-decoration: underline overline line-through; |
| 31 | text-decoration-thickness: 1px; |
| 32 | text-decoration-color: #FFB000; |
| 33 | } |
text-transform changes the capitalization of text without altering the underlying content. This is a presentational change only — screen readers read the original text.
| 1 | .uppercase { |
| 2 | text-transform: uppercase; |
| 3 | letter-spacing: 0.08em; /* improves readability in all-caps */ |
| 4 | } |
| 5 | |
| 6 | .lowercase { |
| 7 | text-transform: lowercase; |
| 8 | } |
| 9 | |
| 10 | .capitalize { |
| 11 | text-transform: capitalize; /* capitalizes first letter of each word */ |
| 12 | } |
| 13 | |
| 14 | .full-width { |
| 15 | text-transform: full-width; /* forces characters into full-width form */ |
| 16 | } |
| 17 | |
| 18 | .full-size-kana { |
| 19 | text-transform: full-size-kana; /* converts small kana to full-size */ |
| 20 | } |
| 21 | |
| 22 | .none { |
| 23 | text-transform: none; /* preserves original casing */ |
| 24 | } |
text-shadow adds drop shadows to text. The syntax is: offset-x offset-y blur-radius color. Multiple shadows can be layered by comma-separating them.
| 1 | /* Simple drop shadow */ |
| 2 | .simple { |
| 3 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); |
| 4 | } |
| 5 | |
| 6 | /* Hard shadow (no blur) */ |
| 7 | .hard { |
| 8 | text-shadow: 4px 4px 0 #00FF41; |
| 9 | } |
| 10 | |
| 11 | /* Glow effect */ |
| 12 | .glow { |
| 13 | text-shadow: 0 0 10px #00FF41, 0 0 20px #00FF41; |
| 14 | } |
| 15 | |
| 16 | /* Layered/depth shadow */ |
| 17 | .layered { |
| 18 | text-shadow: |
| 19 | 1px 1px 0 #222, |
| 20 | 2px 2px 0 #333, |
| 21 | 3px 3px 0 #444; |
| 22 | } |
| 23 | |
| 24 | /* Neumorphism-style */ |
| 25 | .emboss { |
| 26 | text-shadow: |
| 27 | 1px 1px 1px rgba(0, 0, 0, 0.8), |
| 28 | -1px -1px 1px rgba(255, 255, 255, 0.1); |
| 29 | } |
| 30 | |
| 31 | /* Neon text */ |
| 32 | .neon { |
| 33 | color: #00FF41; |
| 34 | text-shadow: |
| 35 | 0 0 5px rgba(0, 255, 65, 0.5), |
| 36 | 0 0 10px rgba(0, 255, 65, 0.3), |
| 37 | 0 0 20px rgba(0, 255, 65, 0.2); |
| 38 | } |
Controlling how text wraps and how white space is handled is critical for responsive design, code display, and preventing layout breaks from long unbroken strings.
white-space
| 1 | .normal { white-space: normal; } /* collapse whitespace, wrap */ |
| 2 | .nowrap { white-space: nowrap; } /* collapse, no wrap */ |
| 3 | .pre { white-space: pre; } /* preserve, no wrap (like <pre>) */ |
| 4 | .pre-wrap { white-space: pre-wrap; } /* preserve, wraps */ |
| 5 | .pre-line { white-space: pre-line; } /* collapse, preserve newlines */ |
| 6 | .break-spaces { white-space: break-spaces; } /* like pre-wrap, preserves spaces */ |
Word Break & Overflow Wrap
| 1 | .break-word { |
| 2 | overflow-wrap: break-word; /* break long words if needed */ |
| 3 | } |
| 4 | |
| 5 | .break-all { |
| 6 | word-break: break-all; /* break at any character */ |
| 7 | } |
| 8 | |
| 9 | .keep-all { |
| 10 | word-break: keep-all; /* don't break CJK words */ |
| 11 | } |
| 12 | |
| 13 | .hyphenate { |
| 14 | hyphens: auto; /* auto-hyphenation */ |
| 15 | } |
| 16 | |
| 17 | .ellipsis { |
| 18 | white-space: nowrap; |
| 19 | overflow: hidden; |
| 20 | text-overflow: ellipsis; /* show ... for overflow */ |
| 21 | } |
| 22 | |
| 23 | /* Multi-line ellipsis (line-clamp) */ |
| 24 | .clamp-3 { |
| 25 | display: -webkit-box; |
| 26 | -webkit-line-clamp: 3; |
| 27 | -webkit-box-orient: vertical; |
| 28 | overflow: hidden; |
| 29 | } |
CSS provides properties for styling ordered and unordered lists, including marker types, positioning, and custom markers using counters and images.
| 1 | /* list-style-type */ |
| 2 | ul { list-style-type: disc; } /* default */ |
| 3 | ul { list-style-type: circle; } |
| 4 | ul { list-style-type: square; } |
| 5 | ul { list-style-type: none; } /* remove markers */ |
| 6 | |
| 7 | ol { list-style-type: decimal; } /* 1, 2, 3 (default) */ |
| 8 | ol { list-style-type: lower-roman; } /* i, ii, iii */ |
| 9 | ol { list-style-type: upper-roman; } /* I, II, III */ |
| 10 | ol { list-style-type: lower-alpha; } /* a, b, c */ |
| 11 | ol { list-style-type: georgian; } /* Georgian numbering */ |
| 12 | |
| 13 | /* list-style-position */ |
| 14 | ol { list-style-position: inside; } /* marker inside the list item box */ |
| 15 | ol { list-style-position: outside; } /* marker outside (default) */ |
| 16 | |
| 17 | /* list-style-image */ |
| 18 | ul { list-style-image: url("arrow.svg"); } |
| 19 | |
| 20 | /* Custom counters */ |
| 21 | .custom-counter { |
| 22 | list-style: none; |
| 23 | counter-reset: my-counter; |
| 24 | } |
| 25 | |
| 26 | .custom-counter li { |
| 27 | counter-increment: my-counter; |
| 28 | } |
| 29 | |
| 30 | .custom-counter li::before { |
| 31 | content: counter(my-counter, upper-alpha) ". "; |
| 32 | color: #00FF41; |
| 33 | font-weight: bold; |
| 34 | } |
Web fonts impact page load performance through file size and rendering delay. Understanding FOIT (Flash of Invisible Text) vs FOUT (Flash of Unstyled Text) is essential for choosing the right loading strategy.
FOIT vs FOUT
| Behavior | Description | Best Avoided By |
|---|---|---|
| FOIT | Flash of Invisible Text — text hidden while font loads | font-display: swap |
| FOUT | Flash of Unstyled Text — fallback font shown, then swapped | Size-match fallback to reduce layout shift |
Performance Strategies
| 1 | /* 1. Preload critical fonts in HTML */ |
| 2 | /* <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin> */ |
| 3 | |
| 4 | /* 2. font-display: swap for body text */ |
| 5 | @font-face { |
| 6 | font-family: "Inter"; |
| 7 | src: url("/fonts/inter.woff2") format("woff2"); |
| 8 | font-display: swap; |
| 9 | } |
| 10 | |
| 11 | /* 3. Subsetting — only load characters you need */ |
| 12 | /* Use unicode-range to split fonts */ |
| 13 | |
| 14 | /* 4. Self-host fonts instead of using CDNs */ |
| 15 | /* Eliminates DNS lookup, reduces connection overhead */ |
| 16 | |
| 17 | /* 5. Use WOFF2 format — best compression (30-50% smaller than WOFF) */ |
| 18 | @font-face { |
| 19 | font-family: "MyFont"; |
| 20 | src: url("/fonts/myfont.woff2") format("woff2"); |
| 21 | } |
| 22 | |
| 23 | /* 6. Fallback font sizing — minimize CLS */ |
| 24 | body { |
| 25 | font-family: "Inter", system-ui, sans-serif; |
| 26 | } |
| 27 | |
| 28 | /* Size-match fallback: adjust fallback to match custom font metrics */ |
| 29 | @font-face { |
| 30 | font-family: "Inter-fallback"; |
| 31 | src: local(system-ui); |
| 32 | size-adjust: 107%; |
| 33 | ascent-override: 90%; |
| 34 | descent-override: 22%; |
| 35 | } |
best practice
pro tip