CSS3 & Responsive Design

Font Properties & Typography Basics

45 min Lesson 9 of 60

Introduction to CSS Typography

Typography is one of the most critical aspects of web design. The way text looks, feels, and reads can make or break a user's experience on your website. Good typography improves readability, establishes visual hierarchy, conveys personality, and keeps visitors engaged. CSS provides a rich set of font and text properties that give you precise control over every aspect of how text is rendered in the browser.

In this lesson, we will cover the core CSS font properties in depth -- from choosing font families and setting sizes to controlling weight, style, spacing, and more. By the end, you will have a thorough understanding of how to style text professionally and create typographic systems that work across different browsers and devices.

The font-family Property

The font-family property specifies which typeface the browser should use to render text. You can specify one or more font names, and the browser will use the first one it finds installed on the user's system. This is why we always use font stacks -- ordered lists of fonts with fallbacks.

Generic Font Families

CSS defines five generic font families. These are not specific fonts; they are categories that the browser maps to a default installed font. You should always end your font stack with one of these as a final fallback:

  • serif -- Fonts with small decorative strokes (serifs) at the ends of letter strokes. Examples: Times New Roman, Georgia. These are considered more traditional and are often used for body text in print and formal contexts.
  • sans-serif -- Fonts without serifs, giving them a cleaner, more modern look. Examples: Arial, Helvetica, Verdana. These are the most popular choice for web body text because they render clearly on screens.
  • monospace -- Every character occupies the same horizontal space. Examples: Courier New, Consolas. These are essential for displaying code, terminal output, and any context where character alignment matters.
  • cursive -- Fonts that mimic handwriting with joined or flowing strokes. Examples: Comic Sans MS, Brush Script. Use these sparingly and never for body text -- they are difficult to read at small sizes.
  • fantasy -- Decorative and display fonts that do not fit other categories. Examples: Impact, Papyrus. These vary wildly between systems and are unreliable for consistent cross-platform rendering.

Example: Generic Font Families

/* Each paragraph uses a different generic family */
.serif-text {
    font-family: serif;
}

.sans-text {
    font-family: sans-serif;
}

.mono-text {
    font-family: monospace;
}

.cursive-text {
    font-family: cursive;
}

.fantasy-text {
    font-family: fantasy;
}

Font Stacks and Fallbacks

A font stack is a comma-separated list of font names in the font-family property. The browser tries each font from left to right and uses the first one that is available. If a font name contains spaces, you must wrap it in quotes. The last font in the stack should always be a generic family name.

Example: Building Font Stacks

/* A modern sans-serif stack */
body {
    font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* A classic serif stack */
.article-body {
    font-family: Georgia, "Times New Roman", Times, serif;
}

/* A monospace stack for code */
code, pre {
    font-family: "Fira Code", "Source Code Pro", Consolas, "Courier New", monospace;
}

/* A system font stack (uses the OS default UI font) */
.system-ui {
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
}
Note: The system font stack shown above is extremely popular because it makes your website look native on every operating system. On macOS it uses San Francisco, on Windows it uses Segoe UI, on Android it uses Roboto, and on Linux it uses the system default.

System Font Stacks

Modern CSS introduced the system-ui keyword, which automatically resolves to the operating system's default UI font. This is a game-changer for performance because no web font needs to be downloaded, and the text feels native to the user's device.

Here is the full system font stack used by many major websites including GitHub:

Example: GitHub's System Font Stack

body {
    font-family:
        -apple-system,        /* Safari on macOS and iOS */
        BlinkMacSystemFont,   /* Chrome on macOS */
        "Segoe UI",           /* Windows */
        "Noto Sans",          /* Linux */
        Roboto,               /* Android */
        "Helvetica Neue",     /* Older macOS */
        Arial,                /* Fallback */
        sans-serif,           /* Generic fallback */
        "Apple Color Emoji",  /* Emoji on Apple devices */
        "Segoe UI Emoji",     /* Emoji on Windows */
        "Noto Color Emoji";   /* Emoji on Linux/Android */
}
Tip: Including emoji font families at the end of your stack ensures that emoji characters render in full color on all platforms. Without them, some browsers might display monochrome emoji outlines.

The font-size Property

The font-size property controls how large text appears. CSS offers many different units for sizing text, and understanding when to use each one is essential for building responsive, accessible websites.

Absolute Units

  • px (pixels) -- A fixed unit that does not scale based on parent elements. 16px is the default browser font size. Pixel values are precise and predictable but do not respect the user's browser font size settings, which can be an accessibility concern.
  • pt (points) -- A print unit (1pt = 1/72 inch). Avoid using points for web design; they are meant for print stylesheets.

Relative Units

  • em -- Relative to the font size of the parent element. If the parent is 16px, then 1.5em = 24px. The em unit compounds -- if you nest elements that each set font-size in em, the sizes multiply, which can lead to unexpectedly large or small text.
  • rem (root em) -- Relative to the font size of the root element (<html>). Unlike em, rem does not compound because it always references the same base value. This makes rem the preferred unit for font sizes in modern CSS.
  • % -- Percentage of the parent element's font size. 100% equals the parent's font size. Like em, percentages compound when nested.
  • vw (viewport width) -- 1vw equals 1% of the viewport width. This creates text that scales with the browser window. Useful for hero headings but dangerous for body text because it can become unreadable on very small or very large screens.

Keyword Values

CSS also provides keyword values for font-size: xx-small, x-small, small, medium, large, x-large, xx-large. The medium keyword equals the browser default (usually 16px). You can also use smaller and larger, which are relative to the parent's font size.

Example: Different Font Size Units

/* Absolute sizing with pixels */
h1 {
    font-size: 32px;
}

/* Relative sizing with rem (recommended) */
h2 {
    font-size: 1.75rem;  /* 28px if root is 16px */
}

/* Relative sizing with em */
.intro {
    font-size: 1.25em;  /* 125% of parent */
}

/* Percentage sizing */
small {
    font-size: 80%;  /* 80% of parent */
}

/* Viewport-based sizing (use with caution) */
.hero-title {
    font-size: 5vw;
}

/* Clamped responsive sizing (best of both worlds) */
.responsive-heading {
    font-size: clamp(1.5rem, 4vw, 3rem);
}
Warning: The em unit compounds when nested. If a parent has font-size: 1.2em and its child also has font-size: 1.2em, the child will actually be 1.2 x 1.2 = 1.44 times the grandparent's size. This compounding effect is the primary reason rem is preferred for font sizing.
Tip: The clamp() function is perfect for responsive typography. It takes three values: a minimum size, a preferred size (usually in vw), and a maximum size. The browser chooses the preferred size as long as it stays within the min and max bounds. This prevents text from being too small on mobile or too large on ultra-wide screens.

The font-weight Property

The font-weight property controls the thickness (boldness) of text. You can use keyword values or numeric values from 100 to 900.

Keyword Values

  • normal -- Equivalent to 400. This is the default weight for most text.
  • bold -- Equivalent to 700. This is the default weight for headings and <strong> elements.
  • lighter -- One weight step lighter than the parent element. The exact result depends on the available font weights.
  • bolder -- One weight step heavier than the parent element.

Numeric Values

Numeric weights go from 100 (thinnest) to 900 (thickest) in increments of 100. Common mappings are:

  • 100 -- Thin / Hairline
  • 200 -- Extra Light / Ultra Light
  • 300 -- Light
  • 400 -- Normal / Regular
  • 500 -- Medium
  • 600 -- Semi Bold / Demi Bold
  • 700 -- Bold
  • 800 -- Extra Bold / Ultra Bold
  • 900 -- Black / Heavy

Example: Font Weight Values

/* Keyword values */
.normal-text {
    font-weight: normal;    /* 400 */
}
.bold-text {
    font-weight: bold;      /* 700 */
}

/* Numeric values */
.thin {
    font-weight: 100;
}
.light {
    font-weight: 300;
}
.medium {
    font-weight: 500;
}
.semi-bold {
    font-weight: 600;
}
.extra-bold {
    font-weight: 800;
}
.black {
    font-weight: 900;
}

/* Relative values */
.lighter-than-parent {
    font-weight: lighter;
}
.bolder-than-parent {
    font-weight: bolder;
}
Note: Not all fonts include all nine weight variations. If you request a weight that the font does not have, the browser will use the closest available weight. For example, if a font only has 400 and 700, setting font-weight: 600 will likely render as 700. Variable fonts solve this problem by offering continuous weight ranges.

The font-style Property

The font-style property controls whether text is displayed in a normal, italic, or oblique style.

  • normal -- The default upright style.
  • italic -- Uses the font's italic variant, which is typically a specially designed version with different letterforms (for example, a single-story "a" instead of a double-story "a").
  • oblique -- Slants the normal font at an angle. Unlike italic, oblique does not use redesigned letterforms; it simply tilts the existing characters. You can optionally specify an angle: oblique 14deg.

Example: Font Style Values

.normal {
    font-style: normal;
}

.italic {
    font-style: italic;
}

.oblique {
    font-style: oblique;
}

/* Oblique with a specific angle (CSS Fonts Level 4) */
.custom-slant {
    font-style: oblique 12deg;
}

/* Common use case: styling a blockquote */
blockquote {
    font-style: italic;
    font-weight: 300;
    border-left: 4px solid #ccc;
    padding-left: 1rem;
}

The font-variant Property

The font-variant property provides access to alternate glyphs and typographic features. The most commonly used value is small-caps, which displays lowercase letters as smaller versions of uppercase letters.

Example: Font Variant and Small Caps

/* Basic small-caps */
.small-caps {
    font-variant: small-caps;
}

/* All small-caps -- even uppercase letters become small caps */
.all-small-caps {
    font-variant-caps: all-small-caps;
}

/* Tabular numbers for aligned columns */
.data-table td {
    font-variant-numeric: tabular-nums;
}

/* Old-style (lowercase) figures for body text */
.body-text {
    font-variant-numeric: oldstyle-nums;
}

/* Ligatures control */
.fancy-text {
    font-variant-ligatures: common-ligatures discretionary-ligatures;
}

/* Combining multiple variants */
.stylish-heading {
    font-variant: small-caps;
    font-variant-numeric: oldstyle-nums;
    letter-spacing: 0.05em;
}
Note: The font-variant-numeric property is incredibly useful for data-heavy designs. Using tabular-nums makes all digits the same width, so columns of numbers align perfectly without needing a monospace font. The oldstyle-nums value produces numbers that have ascenders and descenders, blending more naturally with body text.

The font Shorthand Property

The font shorthand allows you to set multiple font properties in a single declaration. The syntax is:

font: [font-style] [font-variant] [font-weight] font-size[/line-height] font-family;

The font-size and font-family values are required; all others are optional. If you omit any optional value, it resets to its default. This reset behavior is important to understand -- the shorthand will override any individually set properties you declared earlier.

Example: The Font Shorthand

/* Full shorthand */
.complete {
    font: italic small-caps bold 1.2rem/1.6 Georgia, serif;
}

/* Minimal shorthand (only required values) */
.minimal {
    font: 16px Arial, sans-serif;
}

/* With line-height */
.with-line-height {
    font: 1rem/1.5 "Segoe UI", sans-serif;
}

/* System font keywords */
.system-caption {
    font: caption;   /* Uses the OS caption font */
}
.system-menu {
    font: menu;      /* Uses the OS menu font */
}

/* WARNING: The shorthand resets omitted values */
.danger {
    font-weight: 700;
    font-style: italic;
    font: 16px Arial, sans-serif;
    /* font-weight is now 400 (normal) and
       font-style is now normal because the
       shorthand reset them! */
}
Warning: Be cautious with the font shorthand. Because it resets all omitted properties to their defaults, it can unexpectedly undo styles you set individually. Many developers prefer setting font properties individually to avoid this pitfall. If you do use the shorthand, place it before any individual font property overrides.

The line-height Property

The line-height property controls the vertical spacing between lines of text. It is one of the most important properties for readability. The line-height defines the total height of each line box, and the space between the text and the line box edges is distributed equally above and below the text.

Value Types

  • Unitless number (recommended) -- A multiplier of the element's font size. For example, line-height: 1.5 on text with font-size: 16px produces a line height of 24px. Unitless values are inherited as a multiplier, so child elements will calculate their own line height based on their own font size.
  • Length values (px, em, rem) -- A fixed or relative line height. When using em, be aware that the computed value is inherited, not the multiplier. This means child elements with different font sizes may have inappropriate line heights.
  • Percentage -- Similar to em; the computed value is inherited rather than the percentage itself.
  • normal -- The browser's default, typically around 1.2. This is often too tight for body text.

Example: Line Height Values and Best Practices

/* Unitless value (RECOMMENDED) */
body {
    font-size: 16px;
    line-height: 1.6;  /* 25.6px -- great for body text */
}

/* Why unitless is better: */
.parent-em {
    font-size: 16px;
    line-height: 1.5em;  /* Computes to 24px */
}
.child-em {
    font-size: 24px;
    /* INHERITS 24px (the computed value), not 1.5em */
    /* So line-height is 24px for 24px text = cramped! */
}

.parent-unitless {
    font-size: 16px;
    line-height: 1.5;  /* Multiplier inherited */
}
.child-unitless {
    font-size: 24px;
    /* INHERITS the 1.5 multiplier */
    /* So line-height is 24px * 1.5 = 36px -- proper spacing! */
}

/* Different contexts need different line heights */
h1 {
    line-height: 1.2;  /* Tighter for large headings */
}
p {
    line-height: 1.5;  /* Comfortable for body text */
}
.small-print {
    line-height: 1.7;  /* More spacing for small text */
}
Tip: For body text, a line-height between 1.5 and 1.6 is considered ideal for readability. The WCAG (Web Content Accessibility Guidelines) recommends a line-height of at least 1.5 for body text to ensure adequate spacing for people with reading difficulties or visual impairments. For large headings, you can tighten the line-height to 1.1-1.3 to prevent excessive gaps between lines.

The letter-spacing Property

The letter-spacing property adjusts the horizontal space between individual characters (also known as tracking in typography). Positive values increase spacing, and negative values decrease it.

Example: Letter Spacing

/* Increase spacing for uppercase headings */
.uppercase-heading {
    text-transform: uppercase;
    letter-spacing: 0.1em;  /* 10% of font size */
}

/* Slight tracking for readability */
.body-text {
    letter-spacing: 0.02em;
}

/* Tight tracking for large display text */
.display-text {
    font-size: 4rem;
    letter-spacing: -0.02em;
}

/* Fixed pixel spacing */
.fixed-spacing {
    letter-spacing: 2px;
}

/* Normal spacing (reset) */
.normal-spacing {
    letter-spacing: normal;
}
Note: A common typographic rule is to add positive letter-spacing to uppercase text and small-caps text to improve readability. Uppercase letters are designed to be read at the beginning of words (next to lowercase letters), so when an entire word is uppercase, adding a little extra space between the letters makes it easier to read.

The word-spacing Property

The word-spacing property adjusts the space between words. Like letter-spacing, positive values add space and negative values reduce it. This property is less commonly used but can be helpful for fine-tuning justified text or creating specific typographic effects.

Example: Word Spacing

/* Increase word spacing */
.wide-words {
    word-spacing: 0.2em;
}

/* Decrease word spacing */
.tight-words {
    word-spacing: -0.1em;
}

/* Fixed pixel value */
.spaced-words {
    word-spacing: 4px;
}

/* Useful for justified text to reduce river effect */
.justified-text {
    text-align: justify;
    word-spacing: -0.05em;
    hyphens: auto;
}

The font-display Property

The font-display property controls how a web font is displayed while it is loading. This property is specified inside the @font-face rule (which we will cover in detail in the next lesson), but understanding it now is important because it directly impacts typography and user experience.

  • auto -- The browser decides the strategy (usually similar to block).
  • block -- Hides text for up to 3 seconds while the font loads (creates FOIT -- Flash of Invisible Text).
  • swap -- Shows fallback text immediately, then swaps to the web font when it loads (creates FOUT -- Flash of Unstyled Text). Best for important text that must be readable immediately.
  • fallback -- A compromise between block and swap. Gives the font a very short block period (about 100ms), then shows the fallback. If the font loads within about 3 seconds, it swaps in; otherwise, the fallback is used for the rest of the page's lifetime.
  • optional -- The most performance-friendly option. Gives only a very short block period. The browser may decide not to use the web font at all if it does not load quickly enough. Great for slow connections.
Tip: For most websites, font-display: swap is the best default. It ensures text is always visible (good for accessibility and perceived performance) and the web font loads in seamlessly. For purely decorative fonts, consider font-display: optional to prevent layout shifts.

Putting It All Together: A Typography System

Professional websites do not set font properties randomly on individual elements. Instead, they establish a typographic scale -- a consistent set of font sizes, weights, and line heights that create visual harmony across the entire site.

Example: A Complete Typography System

/* Root settings */
:root {
    --font-sans: "Inter", system-ui, -apple-system, sans-serif;
    --font-serif: "Merriweather", Georgia, serif;
    --font-mono: "Fira Code", "Source Code Pro", monospace;

    --text-xs: 0.75rem;    /* 12px */
    --text-sm: 0.875rem;   /* 14px */
    --text-base: 1rem;     /* 16px */
    --text-lg: 1.125rem;   /* 18px */
    --text-xl: 1.25rem;    /* 20px */
    --text-2xl: 1.5rem;    /* 24px */
    --text-3xl: 1.875rem;  /* 30px */
    --text-4xl: 2.25rem;   /* 36px */
}

/* Base typography */
html {
    font-size: 16px;
}

body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    font-weight: 400;
    line-height: 1.6;
    letter-spacing: 0.01em;
}

/* Headings */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-serif);
    font-weight: 700;
    line-height: 1.25;
    letter-spacing: -0.02em;
}

h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }

/* Code */
code, pre {
    font-family: var(--font-mono);
    font-size: 0.9em;
}

/* Utility classes */
.font-light { font-weight: 300; }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }

Exercise 1: Build a Font Stack

Create a CSS rule for a blog article body that uses the following font stack in order: "Libre Baskerville" (a Google Font), Georgia, "Times New Roman", and a serif generic fallback. Set the font size to 1.125rem, the line height to 1.7, and the letter spacing to 0.01em. Then create a separate rule for code elements inside the article that uses a monospace font stack of your choosing with a slightly smaller font size (0.9em) and a background color.

Exercise 2: Create a Typographic Scale

Using CSS custom properties (variables), build a complete typographic scale for a website. Define at least 6 size steps using rem units, a heading font family, a body font family, and a code font family. Then apply these variables to the following elements: body, h1 through h4, p, blockquote (italic, lighter weight, with a left border), and code. Make sure your headings have tighter line-height values than body text, and add appropriate letter-spacing to any text-transform: uppercase elements. Test your scale by creating an HTML page with all these elements and verifying that the visual hierarchy feels natural and consistent.

Exercise 3: Responsive Typography with clamp()

Create responsive headings that scale smoothly between mobile and desktop without using media queries. Use the clamp() function for font sizes on h1 through h3. For h1, use a minimum of 2rem, a preferred size of 5vw, and a maximum of 4rem. For h2, use 1.5rem / 3.5vw / 2.5rem. For h3, use 1.25rem / 2.5vw / 2rem. Add appropriate line-height values that decrease as the heading level increases. Test by resizing your browser window to verify smooth scaling.

ES
Edrees Salih
17 hours ago

We are still cooking the magic in the way!