Skip to main content

🔤 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 font shorthand
  • Tune readability with line-height, letter-spacing, and word-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.

How a font stack falls back The browser tries each family in order — Helvetica Neue, then Helvetica, then Arial — and finally the generic sans-serif if none are installed. Helvetica Neue 1st choice Helvetica fallback Arial fallback sans-serif generic (always works)
Figure 1 — A font stack is tried left to right. Ending with a generic family guarantees text always renders in something reasonable.

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%; }
UnitRelative toCompounds when nested?Best for
pxNothing (absolute)NoFine details like 1px borders — not body copy
emParent's font-sizeYes ⚠️Spacing that should scale with local text
remRoot font-sizeNoAlmost all typography — predictable and accessible
%Parent's font-sizeYesSame 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 rem for sizes; reach for clamp() 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;
}
flowchart LR A["font:"] --> B[style] A --> C[variant] A --> D[weight] A --> E[size] A --> F[line-height] A --> G[family] E --> E1["1rem (required)"] G --> G1["family (required)"] B -.-> B1[italic] C -.-> C1[small-caps] D -.-> D1[700] F -.-> F1["1.5"]

⚠️ 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-spacing sparingly — 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-spaceNew linesSpaces/tabsWraps?Typical use
normalCollapsedCollapsedYesDefault flowing text
nowrapCollapsedCollapsedNoSingle-line labels, ellipsis truncation
prePreservedPreservedNoCode blocks
pre-wrapPreservedPreservedYesPre-formatted text that should still wrap
pre-linePreservedCollapsedYesKeep 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:

  1. 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).
  2. Newspaper: a serif family, justified body, tight heading line-height, and a larger, bold first paragraph.
  3. Modern web: a sans-serif system stack, line-height: 1.6, left-aligned, headings in your --primary-color.
  4. Poster: a huge clamp() heading, text-transform: uppercase with positive letter-spacing, and a tasteful text-shadow.
  5. 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 rem for accessibility, and reach for clamp() for fluid headings.
  • Control weight, style, and variant, but remember the font shorthand resets what you omit.
  • Tune readability with unitless line-height and measured letter-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.