Typography
Master every CSS property that controls how text looks, reads, and responds across devices — from basic font stacks to advanced fluid sizing with clamp().
font-family
The font-family property sets the typeface for an element. You supply a font stack — a prioritized list of family names. The browser walks the list and uses the first font it can find installed or loaded.
Generic Font Families
CSS defines five generic families that act as ultimate fallbacks. You should always end your stack with one.
/* Generic families */ body { font-family: serif; } code { font-family: monospace; } h1 { font-family: sans-serif; } .hand { font-family: cursive; } .fun { font-family: fantasy; }
Font Stacks
A practical font stack lists the ideal font first, similar alternatives next, and a generic last.
body { font-family: "Inter", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } pre, code { font-family: "Fira Code", "Cascadia Code", "JetBrains Mono", Consolas, monospace; }
Font names that contain spaces (e.g. "Fira Code") must be quoted. Single-word names like Arial and generic families like sans-serif should not be quoted.
Serif stack: Georgia, Times New Roman, serif — The quick brown fox jumps over the lazy dog.
Sans-serif stack: Segoe UI, Roboto, Helvetica Neue, Arial — The quick brown fox jumps over the lazy dog.
Monospace stack: Fira Code, Cascadia Code, Consolas — const x = 42;
Cursive stack: Brush Script MT, Segoe Script, cursive — Elegant handwriting feel.
font-size
Controls how large text appears. There are several unit systems; choosing the right one matters for accessibility and responsiveness.
Common Units
/* Absolute */ p { font-size: 16px; } /* Relative to parent */ p { font-size: 1.2em; } /* Relative to root <html> */ p { font-size: 1rem; } /* Percentage (relative to parent) */ p { font-size: 120%; }
16px — This is 16 pixels (absolute).
1.5em — 1.5 × parent = 24px.
1.5rem — 1.5 × root (usually 24px).
120% — 120% of parent = 19.2px.
16px — Still 16px (absolute, ignores parent).
1.5em — 1.5 × 20px = 30px (bigger!).
1.5rem — Still 1.5 × root (unchanged).
120% — 120% of 20px = 24px (bigger!).
Using rem keeps sizing relative to the root, making global adjustments easy and avoiding the compounding problem of nested em values. Set html { font-size: 62.5%; } to make 1rem = 10px for easy math.
font-weight
Sets the boldness of text. Values range from 100 (thinnest) to 900 (heaviest), in increments of 100. Keywords normal maps to 400, bold maps to 700.
h1 { font-weight: 700; } /* bold */ p { font-weight: 400; } /* normal */ .light { font-weight: 300; } .heavy { font-weight: 900; } .bold { font-weight: bold; } /* keyword */ .rel { font-weight: bolder; } /* relative */
100 — Thin: The quick brown fox
200 — Extra Light: The quick brown fox
300 — Light: The quick brown fox
400 — Regular (normal): The quick brown fox
500 — Medium: The quick brown fox
600 — Semi Bold: The quick brown fox
700 — Bold: The quick brown fox
800 — Extra Bold: The quick brown fox
900 — Black: The quick brown fox
Variable fonts support any weight between 1 and 1000 (e.g., font-weight: 450). If a non-variable font doesn't have the exact weight, the browser falls back to the nearest available one.
font-style
Applies italic or oblique styling to text.
.normal { font-style: normal; } .italic { font-style: italic; } .oblique { font-style: oblique; } .angled { font-style: oblique 15deg; } /* custom angle */
normal — Standard upright text.
italic — Uses the italic variant of the font.
oblique — Slants the normal glyphs mechanically.
italic uses specially designed italic glyphs from the font file. oblique simply slants the regular glyphs algorithmically. If no true italic exists, browsers fall back to oblique rendering.
line-height
Sets the vertical spacing between lines of text. Crucial for readability.
/* Unitless (recommended) — multiplier of font-size */ p { line-height: 1.6; } /* With units */ p { line-height: 24px; } p { line-height: 1.5em; } p { line-height: 150%; }
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
When you write line-height: 24px on a parent, children inherit the computed value (24px) regardless of their font size. With unitless line-height: 1.5, children inherit the multiplier, so spacing scales with each element's own font size. Unitless is almost always what you want.
letter-spacing & word-spacing
Fine-tune the horizontal space between characters and words.
.tight { letter-spacing: -0.02em; } .normal { letter-spacing: normal; } .wide { letter-spacing: 0.1em; } .heading { letter-spacing: 0.05em; } .word-tight { word-spacing: -2px; } .word-wide { word-spacing: 8px; }
letter-spacing: -0.05em — Tightly packed characters
letter-spacing: normal — Default character spacing
letter-spacing: 0.1em — Wide character spacing
letter-spacing: 0.25em — Very wide character spacing
word-spacing: -3px — Words pulled closer together
word-spacing: normal — Default word spacing
word-spacing: 12px — Words pushed far apart
text-align
Sets the horizontal alignment of inline-level content inside a block container.
.left { text-align: left; } .center { text-align: center; } .right { text-align: right; } .justify { text-align: justify; }
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
text-align: justify can produce ugly gaps ("rivers of white space") in narrow containers or with long words. Always pair it with hyphens: auto and a lang attribute on the HTML element for best results.
text-decoration
Adds decorative lines to text — underlines, overlines, line-throughs, and more. The shorthand bundles line, style, color, and thickness.
/* Shorthand: line style color thickness */ a { text-decoration: underline; } .fancy { text-decoration: underline wavy #ef4444 2px; } /* Individual longhands */ .custom { text-decoration-line: underline overline; text-decoration-style: dashed; text-decoration-color: #646cff; text-decoration-thickness: 3px; text-underline-offset: 4px; }
underline — The classic link look
overline — Line above the text
line-through — Strikethrough text
underline wavy red — Squiggly error style
underline dotted green — Abbreviation style
underline dashed blue — Custom dashed line
underline double amber — Double underline
all three combined — underline + overline + line-through
text-transform
Changes the capitalization of text visually, without altering the source HTML.
.upper { text-transform: uppercase; } .lower { text-transform: lowercase; } .cap { text-transform: capitalize; } .none { text-transform: none; }
Source: the quick brown fox jumps over the lazy dog
uppercase: the quick brown fox jumps over the lazy dog
lowercase: THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
capitalize: the quick brown fox jumps over the lazy dog
Screen readers ignore text-transform and read the original source text. If you need text to always be uppercase for semantic reasons, type it uppercase in the HTML. Use text-transform only for visual styling.
text-overflow & Multi-line Clamp
Control what happens when text overflows its container.
Single-line Ellipsis
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
Container width: 350px. The text is clipped and "..." appears.
Multi-line Clamp
.line-clamp { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Despite the -webkit- prefix, line clamping works in all modern browsers including Firefox. It is part of the CSS Overflow Module Level 4 draft.
@font-face
Load custom font files directly. This gives you full control over which weights, styles, and formats you serve.
@font-face { font-family: "MyCustomFont"; src: url("fonts/custom.woff2") format("woff2"), url("fonts/custom.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; } /* Bold variant */ @font-face { font-family: "MyCustomFont"; src: url("fonts/custom-bold.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; } /* Usage */ body { font-family: "MyCustomFont", sans-serif; }
font-display Values
font-display: auto; /* browser decides */ font-display: block; /* invisible text until loaded (up to 3s) */ font-display: swap; /* show fallback immediately, swap when ready */ font-display: fallback; /* tiny block period, short swap period */ font-display: optional; /* browser may skip custom font entirely */
For body text, swap provides the best user experience: text is immediately visible in the fallback font, then re-rendered when the custom font finishes loading. For decorative fonts, consider optional.
Google Fonts
Google Fonts hosts free, open-source web fonts. You can load them with a <link> tag or an @import rule.
Method 1: Link Tag (Recommended)
<!-- In the <head> --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
Method 2: CSS @import
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); body { font-family: 'Inter', sans-serif; }
The <link> method is faster because the browser starts downloading the font stylesheet in parallel with the rest of your CSS. @import blocks rendering until the import is resolved, adding latency.
Fluid Typography with clamp()
The clamp() function lets text scale smoothly between a minimum and maximum size based on the viewport width — no media queries needed.
/* clamp(minimum, preferred, maximum) */ h1 { font-size: clamp(1.5rem, 4vw, 3rem); } p { font-size: clamp(0.9rem, 1.5vw, 1.125rem); } /* Fluid spacing too */ .section { padding: clamp(1rem, 3vw, 3rem); }
This Heading Scales Fluidly
Resize the browser window to see this paragraph smoothly adjust. It will not go below 0.85rem or above 1.1rem, but between those bounds it scales proportionally with the viewport width. No media queries required!
Try resizing your browser window to see the text grow and shrink smoothly.
How clamp() Works
/*
clamp(MIN, PREFERRED, MAX)
- Below the breakpoint: font-size = MIN
- In the sweet spot: font-size = PREFERRED (scales with vw)
- Above the breakpoint: font-size = MAX
Example calculation:
clamp(1rem, 2.5vw, 2rem)
At 320px viewport: 2.5vw = 8px -- clamps to 1rem (16px)
At 800px viewport: 2.5vw = 20px -- uses 20px (within range)
At 1400px viewport: 2.5vw = 35px -- clamps to 2rem (32px)
*/A common formula: font-size: clamp(minSize, calc(minSize + (maxSize - minSize) * (100vw - minViewport) / (maxViewport - minViewport)), maxSize). Or simply use the shortcut: pick your min/max sizes and use roughly 2vw to 5vw as the preferred value.
Common Gotchas
Nested em values compound. If a parent is 1.2em and a child is also 1.2em, the child renders at 1.2 x 1.2 = 1.44em of the grandparent. Use rem to avoid this trap.
If you set font-weight: 700 but the loaded font only has 400, the browser will synthesize a "faux bold" that looks thicker but poorly kerned. Always load the actual weight variants you need via @font-face or Google Fonts.
text-overflow: ellipsis alone does nothing. You also need overflow: hidden and white-space: nowrap. All three are required for the ellipsis to appear.
FOUT (Flash of Unstyled Text): fallback font shown, then swapped. FOIT (Flash of Invisible Text): text hidden until font loads. Use font-display: swap and preload critical fonts to minimize these effects.
Form inputs don't always inherit line-height from the body. Always explicitly set line-height on input, textarea, and button elements.
Pro Tips
For maximum performance and a native feel, use the system font stack:font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
For comfortable reading, keep body text between 45 and 75 characters per line. Use max-width: 65ch on your text container — the ch unit equals the width of the "0" character.
Use a mathematical ratio (e.g., 1.25, the "Major Third") to create harmonious heading sizes:h4: 1rem, h3: 1.25rem, h2: 1.563rem, h1: 1.953rem.
Add <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> in your <head> to start downloading fonts before CSS is even parsed.
Use font-variant-numeric: tabular-nums for tabular data (aligns columns of numbers) and font-variant-numeric: oldstyle-nums for body text (numbers that blend with lowercase letters).
Set all margins and padding as multiples of your base line-height to maintain a consistent vertical rhythm. If your line-height is 1.5rem, use 1.5rem, 3rem, and 4.5rem for spacing.