🔤 Text and Typography Properties
Typography is what language looks like on the web. CSS gives you dozens of levers — font family, size, weight, spacing, alignment, and more — to turn a wall of raw text into something legible, expressive, and on-brand. This lesson walks through the properties you will reach for every single day.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Build resilient font stacks with system fonts, web fonts, and generic fallbacks
- Choose the right sizing units (
rem,em,px,%) and understand why relative units aid accessibility - Control weight, style, and variant and combine them with the
fontshorthand - Tune readability with
line-height,letter-spacing, andword-spacing - Apply text-level styling — alignment, decoration, transform, shadow, and overflow — accessibly
Estimated Time: 35–45 minutes • Difficulty: Beginner
Hands-on: Style one block of content three completely different ways using only typography.
In This Lesson
Typography on the Web
Typography is the art of arranging type so written language is legible, readable, and appealing. On the web it does far more than decorate — it establishes hierarchy, signals brand identity, and directly shapes how easily someone can read your content. Get it right and readers never notice; get it wrong and they leave.
💬 "Typography is what language looks like." — Ellen Lupton. Just as different voices can deliver the same sentence with different tone, typography gives your text a visual voice that carries meaning beyond the words.
CSS splits typographic control into two families of properties: font properties (which typeface, how big, how bold) and text properties (alignment, spacing, decoration, and overflow of the rendered text). We will cover both, favoring modern, accessible defaults throughout.
📖 Key Terms
Typeface vs. font: a typeface is the design (e.g. Helvetica); a font is a specific file/variant of it (Helvetica Bold 16px). CSS uses font-family loosely to mean the typeface.
Font stack: the ordered list of families in font-family, tried left to right until one is available.
Web font: a typeface downloaded from a server (via @font-face or a service like Google Fonts) rather than relying on the user's installed fonts.
Font Families & Web Fonts
The font-family property takes a comma-separated stack. The browser uses the first family it can find and falls back to the next. Always end with a generic family so there is a sensible last resort.
/* Specific fonts, then a generic fallback at the end */
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
/* The five classic generics plus two modern keywords */
.serif { font-family: serif; }
.sans { font-family: sans-serif; }
.mono { font-family: monospace; }
.system { font-family: system-ui; } /* the OS's native UI font */
.emoji-safe { font-family: system-ui, "Segoe UI Emoji", sans-serif; }
Think of a font stack like listing your preferred voice actors with understudies: if your first pick is unavailable, the show still goes on with someone similar.
Using web fonts
Beyond fonts the user already has installed, you can ship your own with @font-face or pull them from a service. Prefer the modern WOFF2 format and always set font-display: swap so text is visible while the font loads.
@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-weight: 100 900; /* a variable font covers a whole weight range */
font-style: normal;
font-display: swap; /* show fallback text immediately, swap when ready */
}
body { font-family: "Inter", system-ui, sans-serif; }
<!-- Google Fonts: preconnect first, then load the stylesheet -->
<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=Roboto:wght@400;700&display=swap" rel="stylesheet">
⚠️ Web fonts cost performance
- Each weight and style is a separate download — request only the ones you use.
- A variable font packs many weights into one file; often a net win.
- Preload the font your first paint needs:
<link rel="preload" as="font" type="font/woff2" crossorigin>. - Without
font-display: swap, users can stare at invisible text (the "FOIT" flash).
✅ Best practices for font-family
- Always finish the stack with a generic family (
sans-serif,serif,monospace). - Quote family names that contain spaces:
"Helvetica Neue". - Keep stacks short (3–4 entries) and pick fallbacks with similar metrics to reduce layout shift.
- Order from most to least preferred.
Sizing Text
The font-size property accepts absolute units, relative units, percentages, and keywords. Which you choose has real accessibility consequences.
/* Absolute — ignores user preferences */
h1 { font-size: 32px; }
/* Relative to the PARENT's computed font-size (compounds when nested) */
.child { font-size: 1.25em; }
/* Relative to the ROOT (html) font-size — does NOT compound */
p { font-size: 1rem; } /* usually 16px unless the user changed it */
/* Percentage of the parent's font-size */
small { font-size: 85%; }
| Unit | Relative to | Compounds when nested? | Best for |
|---|---|---|---|
px | Nothing (absolute) | No | Fine details like 1px borders — not body copy |
em | Parent's font-size | Yes ⚠️ | Spacing that should scale with local text |
rem | Root font-size | No | Almost all typography — predictable and accessible |
% | Parent's font-size | Yes | Same role as em, sometimes more readable |
⚠️ Why rem beats px for readable text
When a user bumps their browser's default font size (a common accessibility need), rem- and em-based text scales with it — but text fixed in px ignores them entirely. Reserve px for things that genuinely should not scale, and size body copy in rem.
A modern touch is fluid typography with clamp(), which sets a minimum, a preferred (viewport-scaled) value, and a maximum in one line — no media queries needed:
h1 {
/* never smaller than 1.75rem, never larger than 3rem, fluid in between */
font-size: clamp(1.75rem, 1rem + 3vw, 3rem);
}
✅ Sizing best practices
- Keep body text at least
1rem(~16px) for comfortable reading. - Use a consistent type scale (e.g. a 1.25 ratio) so heading sizes feel harmonious.
- Prefer
remfor sizes; reach forclamp()for responsive headings.
Weight, Style & Variant
font-weight
Weight is the thickness of the strokes — like the volume of a voice. Keywords normal (400) and bold (700) map onto a numeric 100–900 scale.
.thin { font-weight: 100; }
.light { font-weight: 300; }
.regular { font-weight: 400; } /* same as normal */
.semi { font-weight: 600; }
.bold { font-weight: 700; } /* same as bold */
.black { font-weight: 900; }
Not every typeface ships every weight. If a weight is missing, the browser picks the nearest one available. Variable fonts, by contrast, offer a continuous range so any value in between works.
font-style
.normal { font-style: normal; }
.italic { font-style: italic; } /* uses the font's true italic glyphs if present */
.oblique { font-style: oblique 14deg; } /* a mechanical slant of the upright glyphs */
📖 Italic vs. oblique
A true italic is a separately designed set of letterforms (often with cursive flourishes). Oblique just slants the upright design. If a font has real italics, font-style: italic uses them; otherwise the browser synthesizes a slant.
font-variant & small caps
The classic use of font-variant is small caps — capital letterforms sized like lowercase, great for acronyms and refined headings. Modern CSS splits this into more precise sub-properties.
.acronym { font-variant: small-caps; }
/* Granular, modern controls */
.figures {
font-variant-numeric: tabular-nums oldstyle-nums; /* aligned, old-style digits */
font-variant-caps: small-caps;
font-variant-ligatures: common-ligatures;
}
The font Shorthand
The font shorthand sets several properties at once. The order matters, and font-size and font-family are required — omit anything else and it resets to its initial value.
/* order: style variant weight size/line-height family */
p {
font: italic small-caps 700 1rem/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
}
⚠️ The shorthand resets what you leave out
Because omitted values snap back to defaults, the font shorthand can silently wipe an inherited font-weight or font-style. When you only need to tweak one thing, set the longhand property directly.
Spacing for Readability
line-height
line-height sets the vertical space each line occupies. Use a unitless value — it multiplies the element's own font size and, crucially, inherits sanely into children (a unit like 1.5em computes once on the parent and can crowd differently-sized descendants).
body { line-height: 1.5; } /* comfortable for body copy */
h1, h2 { line-height: 1.2; } /* headings look better tighter */
💡 Ruled-paper analogy
Line-height is like the ruling on notebook paper. Too tight and handwriting collides; too loose and lines feel disconnected. Body text usually reads best between 1.4 and 1.6, and longer line lengths want a little more.
letter-spacing & word-spacing
letter-spacing (tracking) adjusts gaps between characters; word-spacing adjusts gaps between words. Prefer em here so the spacing scales with the text.
.badge {
text-transform: uppercase;
letter-spacing: 0.08em; /* all-caps almost always needs positive tracking */
}
h1 { letter-spacing: -0.02em; } /* large display text often wants tighter tracking */
.spaced-out { word-spacing: 0.25em; }
✅ Spacing rules of thumb
- Uppercase and small text benefit from a touch of extra
letter-spacing. - Big headings often want slightly negative tracking.
- Adjust
word-spacingsparingly — it's mostly useful to relieve justified text.
Text-Level Properties
text-align
.article { text-align: left; } /* most readable for LTR languages */
.headline { text-align: center; } /* short blocks only */
.rtl { text-align: right; } /* RTL languages, numeric table columns */
.print { text-align: justify; } /* even edges, but watch for "rivers" */
Left alignment is the safe default for left-to-right languages. Justified text can open ugly "rivers" of whitespace in narrow columns, so use it deliberately.
text-decoration
a { text-decoration: none; }
/* The modern longhands give you full control */
.link {
text-decoration-line: underline;
text-decoration-style: wavy; /* solid | double | dotted | dashed | wavy */
text-decoration-color: var(--primary-color);
text-decoration-thickness: 2px;
text-underline-offset: 3px; /* breathing room under the text */
}
⚠️ Don't remove link underlines carelessly
Underlines help colour-blind users tell links from body text. If you strip them, make links unmistakable another way (weight, a persistent underline on hover/focus, or a distinct colour that also passes contrast).
text-transform
.caps { text-transform: uppercase; }
.title { text-transform: capitalize; }
.lower { text-transform: lowercase; }
Prefer text-transform: uppercase over typing in all-caps in your HTML. The underlying text stays properly cased for screen readers and search engines, and you keep the option to change presentation later.
text-shadow
/* offset-x offset-y blur-radius color */
.hero-title { text-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); }
/* Layer multiple shadows for a glow or outline */
.outline {
color: var(--card-bg);
text-shadow: 1px 1px 0 var(--text-color), -1px -1px 0 var(--text-color);
}
Shadows shine for lifting text off busy hero images, but restraint matters: heavy shadows hurt legibility, and the text should still read acceptably without them.
Overflow: white-space & text-overflow
To truncate a single line with an ellipsis you need three properties working together:
.truncate {
white-space: nowrap; /* keep it on one line */
overflow: hidden; /* clip what spills out */
text-overflow: ellipsis; /* show the tell-tale … */
}
/* Multi-line clamp (widely supported today) */
.clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
white-space | New lines | Spaces/tabs | Wraps? | Typical use |
|---|---|---|---|---|
| normal | Collapsed | Collapsed | Yes | Default flowing text |
| nowrap | Collapsed | Collapsed | No | Single-line labels, ellipsis truncation |
| pre | Preserved | Preserved | No | Code blocks |
| pre-wrap | Preserved | Preserved | Yes | Pre-formatted text that should still wrap |
| pre-line | Preserved | Collapsed | Yes | Keep line breaks, drop extra spaces |
Hands-on Exercise
🏋️ One Story, Three Voices
Objective: Prove that typography alone can transform the feel of identical content.
Instructions:
- Write one short article: an
<h1>, a lead paragraph, and two body paragraphs. Keep the HTML identical across all three versions (duplicate the block three times). - Newspaper: a serif family, justified body, tight heading
line-height, and a larger, bold first paragraph. - Modern web: a sans-serif system stack,
line-height: 1.6, left-aligned, headings in your--primary-color. - Poster: a huge
clamp()heading,text-transform: uppercasewith positiveletter-spacing, and a tastefultext-shadow. - Only typography properties may differ between versions — no changes to layout or content.
💡 Hint
Give each copy of the article a wrapper class (.news, .modern, .poster) and scope every rule to it, e.g. .poster h1 { ... }. For the newspaper drop-cap, target the first paragraph's first letter with .news p:first-of-type::first-letter.
✅ Sample solution (the poster heading)
.poster h1 {
font-family: system-ui, sans-serif;
font-size: clamp(2.5rem, 1rem + 8vw, 5rem);
font-weight: 900;
line-height: 1.05;
text-transform: uppercase;
letter-spacing: 0.04em;
text-align: center;
color: var(--primary-color);
text-shadow: 0 3px 10px rgba(0, 0, 0, 0.35);
}
.news p:first-of-type::first-letter {
font-size: 3.2em;
float: left;
line-height: 0.8;
padding-right: 0.08em;
font-weight: 700;
}
🎯 Quick Quiz
Question 1: Why is rem usually preferred over px for body text?
Question 2: Which three properties together truncate a single line of text with an ellipsis?
Question 3: Why use text-transform: uppercase instead of typing the text in capitals in your HTML?
Summary & Quiz
🎉 Key Takeaways
- Build font stacks that end in a generic family; ship web fonts as WOFF2 with
font-display: swap. - Size text in
remfor accessibility, and reach forclamp()for fluid headings. - Control weight, style, and variant, but remember the
fontshorthand resets what you omit. - Tune readability with unitless
line-heightand measuredletter-spacing. - Apply alignment, decoration, transform, shadow, and overflow with accessibility in mind.
📚 Further Reading
🚀 What's Next?
Next we turn from letterforms to hue: Colors, Backgrounds, and Gradients — every way CSS lets you specify colour, plus backgrounds and the three kinds of gradients.
🎉 Well done!
You can now give any block of text a deliberate visual voice. On to colour.