CSE 134B Web Client Languages
  1. Presentation
  2. Fonts
  3. Fonts in CSS

Presentation · Fonts 05

Fonts in CSS

A few CSS properties turn a design's choice of typeface into text on someone else's screen. They are older and stranger than most of CSS, because they must work on devices the author has never seen, with fonts the author cannot know are there. This page is about writing them so the text reads well whatever the device has.

The earlier pages chose typefaces; this one asks for them: font-family and the way a browser walks a list of names character by character, the generic families, the stacks people ship, weight, style and width, the faces browsers fake, the font shorthand and its traps, font-size-adjust, and a little on case and underlines. Loading the files is the subject of Web fonts and performance; sizes and spacing belong to Size, spacing and measure.

1. font-family and the font stack

The font-family property takes a comma-separated list of family names, in order of preference. The list is usually called a font stack, and the browser uses the first name it can find on this device, or among the page's own web fonts, for each piece of text:

body { font-family: "Literata", Georgia, "Times New Roman", serif; }

Read from left to right, the stack says: Literata if the page has loaded it; otherwise Georgia, installed on nearly every Mac and Windows computer; otherwise Times New Roman; and if none of those exist, whatever this browser uses as its serif. The last item is a generic family, a keyword rather than a font name, and it is the only item guaranteed to mean something on every device. Anatomy and vocabulary introduced the generic families as CSS's own small classification; this page explains how they behave.

The property inherits, so it is usually set once on body and changed only where a different voice is wanted. Names are compared without regard to case. A misspelled name is not an error: the browser simply does not find it and moves on, which is why every stack needs a sensible last item.

Quoting family names

Family names may be quoted or left bare, and the rules are short:

  • Quoting is always allowed for a font name, and is the safe habit for any name with spaces, digits or punctuation: "Times New Roman", "Source Code Pro".
  • Bare names are allowed when every word is a valid CSS identifier. Times New Roman works unquoted, because it is three identifiers and CSS joins them with single spaces. A word that starts with a digit, such as Font 2, is not an identifier, and the whole declaration is thrown away.
  • Generic families must not be quoted. "serif" in quotes asks for a font whose name is serif, which almost certainly does not exist. The same goes for CSS-wide keywords: a font actually named Inherit would have to be written "Inherit".
h1 { font-family: "Helvetica Neue", "sans-serif"; } /* a font named sans-serif */ p { font-family: Literata 2, serif; } /* invalid: the whole line is dropped */ code { font-family: Menlo, Consolas; } /* no generic: the default, often a serif */

The last line is the most common mistake and the hardest to see: on the author's Mac, Menlo is there; on a Linux laptop with neither font, the code falls back to the default font, usually a proportional serif, and its alignment falls apart.

2. How the browser finds a font for each character

It is tempting to think of a stack as choosing one font for an element. The CSS Fonts specification describes something finer: for each character, the browser "iterates through the list of family names until it matches an available font that contains a glyph for the character to be rendered." A font is chosen per character, or more exactly per grapheme cluster, the unit a reader perceives as one letter (a base character with its accents, or an emoji built from several code points).

For each family in the list, in order, the browser:

  1. finds the family: a generic keyword maps to the browser's choice; any other name is looked up among the page's @font-face rules, then among installed fonts;
  2. picks one face from the family by width, then style, then weight (sections 5 to 7 below);
  3. checks whether that face has a glyph for the character, downloading it first if it is a web font whose unicode-range includes the character;
  4. uses that face if it has the glyph, and otherwise moves on to the next family.

If every family in the list lacks the character, the browser runs its own system font fallback, searching the fonts installed on the device for one that has it. Only if nothing on the system can draw it does the reader see a placeholder: an empty box often called tofu, or a symbol from a "last resort" font that shows which block of Unicode the character belongs to. A short history of type tells how the Noto project set out to end tofu.

Per-character fallback for one line of mixed textThe line "Café Ωμέγα 東京 → ✓ 😀" set in the stack Inter, Georgia, sans-serif, as Chrome on macOS drew it. A grid has one row per run of text and one column per step: Inter, a Latin-only web font; Georgia, installed; sans-serif, which here is Helvetica; and the system's own fallback. Café: Inter has it and draws it. Ωμέγα: Inter lacks Greek, Georgia has it and draws it. 東京: Inter, Georgia and Helvetica all lack it; system fallback draws it in PingFang SC. The arrow and check mark: the three fonts lack them; system fallback uses Lucida Grande. The emoji: only system fallback has it, in Apple Color Emoji.font-family: Inter, Georgia, sans-serifInterweb font, Latin onlyGeorgiainstalledsans-serifhere: Helveticasystem fallbackthe OS searchestried in this orderCafédraws itΩμέγαno Greekdraws it東京missingmissingmissingPingFang SC→ ✓missingmissingmissingLucida Grande😀missingmissingmissingApple Color EmojiOne line, five fonts. Each character takes the first family that has it; the system fills the rest.
One line of text in a three-item stack, as Chrome 156 on macOS rendered it (read from DevTools). Inter's Latin-only file draws the Latin; everything else falls through, character by character.

Two consequences follow. First, text your fonts do not fully cover is drawn in several typefaces, which beats a missing glyph but can make a carefully chosen face look patched, as Why type matters shows. Second, the families after the first are not only for when it fails to load; they also draw what it lacks, so choose them with the page's other scripts in mind.

When a family counts as missing

A few details of the matching rules surprise people:

  • A failed web font is treated as absent; while one is still downloading, font-display decides what is drawn (Web fonts and performance).
  • Only the chosen face is checked. If a family's bold face lacks a character its regular has, the browser moves to the next family rather than borrowing from the regular.
  • Accents travel with their letter. Cluster matching draws a base letter and its combining marks from one font, so the accent sits where its designer put it.
  • Language can change the fallback, as section 12 shows.

3. Generic families

A generic family names a kind of typeface, not a typeface. The specification calls each one an alias for one or more locally installed fonts, which may be a composite that changes with the character, the content language, the user's preferences and the system settings. In plain terms: a generic family is the browser's choice, and often the reader's. The author says what kind of face to use; the device says which one.

CSS Fonts Level 4 sorts the generics into three groups, and the grouping explains how to use them:

Generic families in CSS Fonts Level 4, and their support as of October 2026
GroupKeywordsWhat the spec promisesSupport
Completeserif, sans-serif, monospace, cursive, fantasy, system-ui, mathCover all of Unicode and always match an installed fontAll browsers; system-ui Baseline since 2021, math since March 2026
Incompleteui-serif, ui-sans-serif, ui-monospace, ui-roundedCover all of Unicode but may match no font at all on some systemsSafari only, since 13.1; Chrome and Firefox treat them as unknown names
Script-specificgeneric(fangsong), generic(kai), generic(nastaliq), generic(khmer-mul)Apply to one writing system and may match nothingNew in the current draft; Chrome 156 rejects the generic() syntax

The practical rule follows from the groups. End every stack with a complete generic, and put an incomplete one earlier if you want it, where a browser that does not know it will skip it. The generic you end with should match the kind of face you asked for, so that a missing serif web font becomes some serif and a missing code font becomes some monospace.

The list has changed recently, and older material is out of date in two places. The specification once had an emoji generic family; the current draft dropped it, "because the font-variant-emoji property covers the desired functionality better" (The details, page 09, covers that property). And fangsong, a Chinese style between Song and Kai used for official documents, is now spelled generic(fangsong), a functional form chosen so that new generic names cannot clash with real font names. In Chrome 156 both bare words are simply treated as font names that do not exist. The course's own chapter 8 lists a fifth ui-system generic, which is not a CSS keyword; the system font is system-ui.

system-ui

system-ui is the font the operating system uses for its own interface: San Francisco on Apple systems, Segoe UI on Windows, Roboto on Android, and whatever the desktop is set to on Linux. It costs nothing to download, it matches the menus and dialogs around the page, and it has been in every major browser since Firefox 92 in 2021, which made it Baseline widely available in March 2024. A short history of type tells how the web came back to the system font.

The specification adds two caveats. It is "intended for use with UI elements in web applications, and may or may not be suitable for large paragraphs of text". And it follows the system's language settings rather than the page's: Simplified Chinese users on older Windows "might get a monospaced serif font". For interfaces it is excellent; for long reading, a deliberate text face is usually better.

The ui-* families and math

The four ui-* generics ask for the system's interface type in a particular style. On Apple systems ui-serif is New York, ui-monospace is SF Mono and ui-rounded is SF Rounded; other platforms have not implemented them, and the specification says ui-rounded is not expected to map to anything on platforms without a rounded system font. Write them first, as a request, never last. math, Baseline newly available since March 2026, asks for a font designed for mathematical notation, with the extra tables MathML needs; in Chrome on macOS it maps to STIX Two Math.

4. Building a stack

A good stack is a short argument about the devices a page will meet. It usually has four parts: the face you want, a close substitute or two that are likely to be installed, the platform faces that keep the same character, and a generic family of the same kind.

System font stacks

Before system-ui existed, sites that wanted the operating system's font listed every platform's font by name. Variations of this stack were copied across the web in the late 2010s:

body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }

Each name was there for a reason: -apple-system was Safari's private keyword for San Francisco, BlinkMacSystemFont was Chrome's on the Mac, and the rest are the system faces of Windows, Android and three Linux desktops. Today one keyword does the job, and the long list is unnecessary for current browsers, though harmless:

body { font-family: system-ui, sans-serif; }

Modern Font Stacks

A system stack need not look like the system's interface. Modern Font Stacks, a collection by Dan Klammer released under a CC0 license, groups the fonts already installed on macOS, Windows, Android and Linux by classification, and offers a stack for each: transitional, old style, humanist, geometric humanist, neo-grotesque, slab serif, Didone, monospace and more. Its pitch: "The fastest fonts available. No downloading, no layout shifts, no flashes". Two of its stacks, copied from the site:

/* Transitional */ font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif; /* Neo-Grotesque */ font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;

The transitional stack is a small piece of type research: Charter ships with macOS, Bitstream Charter with many Linux systems, Sitka Text and Cambria with Windows, all sturdy transitional serifs, so the page keeps its voice across systems. That is the honest limit of a system stack: the reader sees a similar face, not the same one. Where exact letterforms matter, a web font guarantees them; where speed matters more, a system stack costs no bytes.

A stack for a web font

When the first item is a web font, the rest of the stack has three jobs: draw the page while the font loads, draw it for good if the font fails, and draw the characters the font lacks. A stack built for those jobs might look like this:

:root { --font-text: "Literata", "Literata fallback", Georgia, "Noto Serif", serif; }

"Literata fallback" is a local font resized to Literata's metrics so the swap does not shift the layout (Web fonts and performance). Georgia covers Apple and Windows, Noto Serif covers Android, Linux and many more scripts than a Latin subset, and serif catches the rest.

Try a stack

Browsers deliberately do not tell a page which font drew its text. The list of fonts installed on a device is one of the signals that can identify a person across sites, so exposing it would help trackers (Privacy covers fingerprinting as a quality concern). The previewer below works around this the way such tools do, by measuring text on a canvas, and labels its answers as inferences. DevTools has the real answer: in Chrome, select an element and look at the bottom of the Computed tab for "Rendered Fonts"; in Firefox, open the Fonts panel.

5. font-weight

font-weight asks for a face by how heavy its strokes are. It accepts a number from 1 to 1000, the keywords normal (400) and bold (700), and the relative keywords bolder and lighter. The traditional names map to hundreds: 100 Thin, 300 Light, 400 Regular, 500 Medium, 600 Semibold, 700 Bold, 900 Black, with the full list in Anatomy and vocabulary. Numbers between the hundreds, such as 450 or 650, only make a difference with a variable font that has a weight axis, or a family with faces declared at those weights.

Weight also carries tone. The lecture's slide lists the associations: bold reads as loud, important and authoritative; thin as clean, sophisticated and trustworthy. They are tendencies of a culture, not laws.

bolder and lighter

The relative keywords do not add a fixed amount. They look at the weight inherited from the parent and jump to the next band, by a table in the specification:

Relative weights in CSS Fonts Level 4
Inherited weightbolder giveslighter gives
below 100400no change
100 to 349400100
350 to 549700100
550 to 749900400
750 to 899900700
900 and aboveno change700

Browsers' default style sheets use bolder for <b> and <strong>, so a <strong> inside a 600-weight heading asks for 900, a weight the family may not have. Many sites, this one included, set an explicit number for strong instead.

When the weight is not there

A family rarely has every weight, and the browser must pick one it does have. The specification's rule is a search that depends on which side of the regular weight the request falls:

  • Below 400: look at lighter weights first, in descending order, then heavier ones.
  • From 400 to 500: look upward to 500 first, then lighter weights in descending order, then weights above 500.
  • Above 500: look at heavier weights first, in ascending order, then lighter ones.

Light requests stay light, bold requests stay bold, and regular stays near regular. The diagram follows three requests through a family with only Light (300) and Bold (700):

How a browser picks a weight the family does not haveA weight scale from 100 to 900, with the family's only two faces marked at 300 and 700. Three requests. A request for 200 searches downward from 200 and finds nothing, then searches upward and finds 300. A request for 450 searches upward from 450 to 500 and finds nothing, then downward and finds 300, so 450 and 500 both get the Light face. A request for 600 searches upward and finds 700. Chrome produced exactly these results in a test with two real faces.100200300400500600700800900font-weight requestedLight 300Bold 700faces the family has2001. down: nothing; 2. up: 3004501. up to 500: nothing2. down: 3006001. up: 700
With only 300 and 700 available, a request for 200 gets 300, a request for 400, 450 or 500 also gets 300, and anything above 500 gets 700. Chrome 156 matched these exactly.

Note that 500 lands on Light, rarely what a designer meant. The fix is declaring the faces the design uses: an @font-face rule per weight, or a range such as font-weight: 100 900 for a variable font (Web fonts and performance).

6. font-style: italic and oblique

font-style has three values. normal asks for an upright face. italic asks for an italic, a separately drawn design that descends from handwriting, often with different letterforms: in many serif italics the a loses its upper storey and the f grows a tail below the baseline (A short history of type tells where the italic came from). oblique asks for the upright design slanted, which is what most sans-serif families provide, and it can take an angle: oblique 10deg. Without an angle it means 14 degrees.

Matching follows the same spirit as weight. A request for italic takes an italic face if the family has one, then an oblique, then the upright face. A request for oblique takes an oblique if there is one, then an italic, then upright. For variable fonts, an ital axis satisfies italic and a slnt axis satisfies an oblique angle; Variable fonts, page 08, covers both.

If the family has no sloped face at all, the browser may fake one by slanting the upright letters: a double-storey a leaning over instead of an italic a. Section 8 takes that up.

7. font-stretch, now also font-width

Some families come in several widths: condensed faces for headlines in narrow columns, expanded ones for display. font-stretch picks among them, by percentage from 50% to 200% or by keyword, from ultra-condensed (50%) through condensed (75%) and normal (100%) to ultra-expanded (200%). It does not squeeze letters; if the family has no condensed face, nothing changes, because there is no synthesis for width. It is matched first, before style and weight, and for a variable font with a width axis it sets that axis.

CSS Fonts Level 4 renamed the property font-width, a clearer name, and kept font-stretch as an alias. As of October 2026, Safari (since 18.4) and Firefox (since 155) accept the new name and Chrome does not, while font-stretch works everywhere. Write font-stretch for now. Choosing and pairing explains why condensed faces suit labels and not paragraphs.

8. font-synthesis: faux bold and italic

When a family has no face for the weight or style requested, browsers do not give up; they synthesize one from the face they have. A faux bold thickens each outline. A faux italic slants the upright letters. The specification is frank about the result: a synthetic oblique "is not equivalent to a true oblique, in which optical stroke thicknesses are properly preserved despite the slant", and authors should be aware that synthesis "might not be suitable for scripts like Cyrillic, where italic forms are very different in shape."

In Chrome 156, a single-weight face asked for 550 was drawn as it was, and asked for 600 was thickened, so faux bold starts at 600. The thickened letters kept their regular widths, so faux bold looks crowded. And a synthesized oblique 30deg had exactly the slant of a synthesized italic: the angle does not reach the fake.

The font-synthesis property says which fakes are allowed. It is a shorthand for four longhands, all auto by default:

The font-synthesis longhands
LonghandAllows the browser to fakeSupport
font-synthesis-weightBold, by thickening the outlinesBaseline widely available since September 2025
font-synthesis-styleItalic or oblique, by slantingBaseline widely available since September 2025
font-synthesis-small-capsSmall capitals, by shrinking full capitals, for font-variant-capsBaseline widely available since September 2025
font-synthesis-positionSuperscripts and subscripts, for font-variant-positionFirefox only

The shorthand takes none or a list of what to allow: font-synthesis: weight allows faux bold only; font-synthesis: none allows nothing. Its own support is older, Baseline widely available since July 2024.

/* A display face with one weight and no italic: forbid both fakes. */ .brand { font-family: "Bungee", system-ui, sans-serif; font-synthesis: none; } /* Chinese, Japanese and Korean fonts almost never have italics. */ :is(:lang(zh), :lang(ja), :lang(ko)) { font-synthesis-style: none; }

Turning synthesis off is not automatically better: a request the family cannot meet is then ignored, and the emphasis disappears. The real fix is to load the faces the design uses, and to forbid fakes where they are worse than nothing. "Chinese, Japanese and Korean fonts almost always lack italic or oblique faces," the specification notes, and slanted Chinese characters are not a convention readers expect. The previewer above compares real and fake bold italic in Literata.

9. The font shorthand

The font property sets style, small capitals, weight, width, size, line height and family in one declaration, in an order taken from the traditional printer's notation, "12pt/14pt Times":

The parts of the font shorthand, and what it resets without being askedThe declaration font: italic 600 1.125rem/1.5 "IBM Plex Sans", sans-serif, divided into parts. italic and 600 are optional and may come in any order, together with small-caps and a width keyword such as condensed, but only before the size. 1.125rem is the size and is required. Slash 1.5 is the line height, optional, and only directly after the size. "IBM Plex Sans", sans-serif is the family list, required and always last. Below, two boxes. Reset to their initial values whenever font is used: line-height if not given, every font-variant longhand, font-kerning, font-size-adjust, font-feature-settings, font-variation-settings, font-optical-sizing and font-language-override. Not touched: font-synthesis and font-palette.font:italic6001.125rem/1.5"IBM Plex Sans", sans-serifoptional, any order:style, small-caps,weight, width keywordsize:requiredline height:optionalfamily list: required, lastReset to initial values, silentlyline-height (to normal, if not given)font-variant-* (all seven), font-kerningfont-size-adjust, font-feature-settingsfont-variation-settings, font-optical-sizingNot touchedfont-synthesis-*font-palette
Size and family are required; the rest are optional. Anything the declaration does not name goes back to its initial value, including properties it cannot set at all.

Three rules cause most of the trouble:

  • Size and family are required. font: bold Georgia, serif has no size, so it is invalid, and the browser discards the whole declaration, leaving the inherited font in place without a warning.
  • Only the old values fit. The shorthand accepts small-caps but none of the newer font-variant values, and width keywords such as condensed but not percentages.
  • It resets what it does not mention. The specification divides font properties into those the shorthand sets, those it resets ("a reset-only sub-property"), and those it leaves alone. The reset list is long: line-height when no / value is given, every font-variant-* longhand, font-kerning, font-size-adjust, font-feature-settings, font-variation-settings, font-optical-sizing and font-language-override. font-synthesis and font-palette are untouched.
body { line-height: 1.6; font-variant-numeric: tabular-nums; } .price { font: 600 1.25rem "IBM Plex Sans", sans-serif; /* line-height is now normal; tabular-nums is gone */ }

In Chrome, that rule computed line-height and font-variant-numeric back to normal, and reset kerning, feature, variation and size-adjust settings too, while font-synthesis kept its inherited value. The habit: use the longhands in components, and keep the shorthand for deliberate resets.

System font keywords

The shorthand alone accepts six keywords that copy a font from the operating system's interface: caption, icon, menu, message-box, small-caption and status-bar. They come from CSS2 and work everywhere, but what they give is up to the browser, and Chrome 156 on macOS gave all six the same answer, Arial at 16 pixels. For the system's look, system-ui is the reliable tool; the keywords are worth recognizing in old style sheets and little else.

Form controls and font: inherit

One use of the shorthand is close to universal. Buttons, inputs, selects and text areas do not inherit the page's font in the browser's default styles; in Chrome on macOS a button's computed font is Arial at 13.33 pixels, whatever the page around it uses. A reset rule fixes that by letting the shorthand do what it does best, set everything at once:

@layer base { :is(button, input, select, textarea) { font: inherit; } }

10. font-size-adjust

Anatomy and vocabulary showed that the x-height, the height of the lowercase, decides how large a font looks far more than its nominal size does. Two fonts at the same font-size can look quite different in size, and that difference becomes a problem whenever one font stands in for another: a fallback before a web font loads, a substitute on a device without the font, or a second font drawing the characters the first one lacks.

The ratio of x-height to font size is called the aspect value. It is about 0.52 for IBM Plex Sans and Arial, 0.51 for Literata, 0.48 for Georgia, 0.45 for Times, 0.55 for Verdana and 0.42 for Courier New, measured in Chrome. Inter shows that the number can move: its optical size axis gives it about 0.55 at text sizes (an x-height of 1,118 units in 2,048) and 0.52 at display sizes of 32 pixels and up. So at the same size, Verdana's lowercase is about a fifth taller than Times'. font-size-adjust tells the browser to scale each font so that its x-height comes out the same:

body { font-family: "Literata", Georgia, "Times New Roman", serif; /* Scale every font in the stack to Literata's x-height. */ font-size-adjust: from-font; } .caption { /* Or name the aspect value directly: lowercase half the font size. */ font-size-adjust: 0.5; }

With a number, every font in the stack is resized so that its x-height is that fraction of font-size. With from-font, the browser takes the number from the first available font, so the fallbacks are scaled to match the face the designer chose. CSS Fonts Level 5 also lets you match a different metric, such as cap-height or ic-width for the width of CJK ideographs, by naming it first: font-size-adjust: cap-height from-font.

Firefox has supported the number since version 3; Chrome added the property in version 127, in July 2024, making it Baseline newly available, from-font included. It complements the size-adjust descriptor (Web fonts and performance), which fits one fallback to one web font's widths, while font-size-adjust evens out the lowercase across a whole stack. The specification warns that too large an x-height cramps diacritics.

11. Case and underlines

Two text properties work closely with fonts and have details that are easy to miss. CSS properties introduced both; this section adds what matters for type.

text-transform speaks languages

text-transform: uppercase is the right way to set a label in capitals, keeping the source in normal case, for the reasons Choosing and pairing gives. What is easy to forget is that case mapping depends on the language, and the browser uses the element's lang to do it. A Chrome test with uppercase on four elements:

  • Turkish has a dotted and a dotless i in both cases. With lang="tr", "istanbul ılık" became "İSTANBUL ILIK", keeping the dot on the capital İ; with lang="en", the same text became "ISTANBUL ILIK", which in Turkish is a different letter.
  • Greek drops most accents in capitals. With lang="el", "Αθήνα ώρα" became "ΑΘΗΝΑ ΩΡΑ" in Chrome; Firefox does the same, and Safari keeps the accents.
  • German ß has no single capital in traditional use, and "Straße" became "STRASSE", one letter longer.
  • Dutch treats ij as one letter at the start of a word. capitalize gives "IJsland" in Firefox and Safari, and "Ijsland" in Chrome.

capitalize capitalizes every word, after hyphens too, so "the lord of the rings, o'neill, e-mail" becomes "The Lord Of The Rings, O'neill, E-Mail". English title case follows style-guide rules no CSS property knows; write titles in title case in the HTML.

Underlines that fit the font

A default underline is drawn by the browser from the font's metrics, and it often sits too close to the letters and runs through descenders. Four properties shape it to the typeface:

a { text-decoration-line: underline; text-decoration-thickness: from-font; /* or 0.08em */ text-underline-offset: 0.2em; text-decoration-color: color-mix(in oklab, currentColor 60%, transparent); text-decoration-skip-ink: auto; /* the default: gaps at descenders */ }
  • text-decoration-thickness takes a length, a percentage of 1em, or from-font, which uses the thickness the type designer put in the font. Use em so the line grows with the text.
  • text-underline-offset moves the line away from the baseline. text-underline-position: under instead puts it below the descenders, useful for text with many of them and in some mathematical and Asian contexts.
  • text-decoration-skip-ink: auto, the default, interrupts the underline where it would cross a descender, so a g or a y stays legible.

The longhands are Baseline widely available. Safari accepted color, style and thickness inside the text-decoration shorthand only in version 26.2, in December 2025, so the longhands are the safer way to write them for now. Whatever the styling, keep the underline on links in running text; color alone is not enough (WCAG 1.4.1, as Choosing and pairing explains).

12. Fonts for every reader and every language

Every property on this page is a request, and the reader's device and settings have the last word. That is a feature: it is how people who need different type get it.

Readers who choose their own fonts

  • Browser settings. Chrome's appearance settings let a reader choose the standard, serif, sans-serif and fixed-width fonts and a minimum font size. Those choices become the generic families, so a stack that ends in serif lands on the reader's chosen serif. Firefox goes further: its font settings include a checkbox, "Allow pages to choose their own fonts, instead of your selections above", and a reader who clears it sees every page in their own fonts.
  • User style sheets and extensions can replace fonts on every site, for example with a face a reader with dyslexia finds easier, or a larger, heavier one for low vision. Type for people, page 10, looks at what the research says about such faces.
  • Reader modes discard the page's fonts altogether and set the text in the reader's preferred face and size.

Pages that survive these choices end their stacks in generics, do not depend on one font's exact widths, and do not put meaning in font glyphs: an icon font turns into wrong letters or boxes when a reader overrides fonts, a known problem in Firefox. SVG icons do not have it.

Other scripts, and the lang attribute

Generic families and system fallback make a page readable in scripts its author never planned for: a Hebrew comment or a name in Devanagari is drawn by whatever font the reader's system has for it. The author can help the browser choose well, mainly with the lang attribute.

Chinese, Japanese and Korean share thousands of characters through Han unification, the decision to give a character one Unicode code point even when the conventions for drawing it differ between countries. The same code point can therefore look different in a Japanese font and a Chinese one, and a reader notices the wrong one. Chrome on macOS drew the same three characters, 直骨今, in three different fonts depending only on lang: Hiragino Kaku Gothic for lang="ja", PingFang SC for lang="zh-CN" and PingFang TC for lang="zh-TW", all with font-family: sans-serif.

<p>The character <span lang="ja">直</span> in Japanese, and <span lang="zh-CN">直</span> in Simplified Chinese.</p>

Mark up the language of the document and of any passage in another language, and use :lang() where a script needs its own stack or settings, such as turning off faux italic for Chinese. CSS for people covers :lang(), and Type for people, page 10, goes deeper into scripts, vertical text and mixing them.

13. This site's stack, as a worked example

Every page of this course is set in IBM Plex Sans, with IBM Plex Mono for code. Web fonts and performance looked at how the files load; here is how the CSS asks for them. The theme defines the stacks once, as custom properties:

--font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; --font-body: var(--font-sans); --font-heading: var(--font-sans); --font-mono: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace; --font-code: var(--font-mono);

The sans stack reads: Plex if loaded; Safari's ui-sans-serif; the system UI font in every current browser; two older names for it, reached only by browsers too old for system-ui; and any sans-serif. The mono stack follows the same pattern, and both end in a complete generic of the right kind.

The weights are declared one face at a time. Plex Sans has five rules: 400, 500, 600 and 700 upright, and 400 italic; Plex Mono has 400, 500 and 600, with no italic. Headings use 600. The Vanilla Breeze framework underneath sets strong and b to 600 rather than bolder, so bold text never asks for a face the site does not have, and it turns synthesis off for emphasis:

strong, b { font-weight: 600; font-synthesis: none; } em, i { font-style: italic; font-synthesis: none; } :where(b i, i b, strong em, em strong) { font-weight: 600; font-style: italic; font-synthesis: none; }

Those few lines show the trade-off from section 8 in practice. The site never shows a faux bold or a faux italic. But the only italic declared is 400, so a Chrome test reproducing these rules found three places where a request goes quietly unmet:

  • Bold italic (<strong><em>) is drawn in the regular italic, so it looks like plain emphasis. With synthesis allowed, Chrome would have thickened the italic instead.
  • Italic in a heading drops from the heading's 600 to the italic's 400, so an emphasized word in a heading is lighter than its neighbors.
  • Italic in code stays upright, because Plex Mono has no italic face declared and none may be faked.

None of these is exactly wrong; each says real faces matter more than complete emphasis, and each extra italic weight would be another file of about 24 KB, the size of the Plex italic. The habit to take away: know which faces your CSS declares and which your content asks for, and check the gap in DevTools.

14. Summary

Fonts in CSS, in brief
ConceptKey points
font-familyA list in order of preference, ending in a generic family. Quote names with spaces, digits or punctuation; never quote generics. A misspelled name fails silently.
MatchingPer character: the first family whose chosen face has the glyph draws it; then system fallback; then tofu. Later families also cover what the first lacks.
Generic familiesThe browser's and reader's choice. Complete generics (serif, sans-serif, monospace, cursive, fantasy, system-ui, math) end a stack; ui-* are Safari only; emoji was dropped and fangsong is now generic(fangsong).
Stackssystem-ui, sans-serif replaces the old system stacks. Modern Font Stacks groups installed fonts by classification. A web-font stack adds a matched fallback, platform faces and a generic.
font-weight1 to 1000; bolder/lighter jump bands. Missing weights: below 400 search lighter first, 400 to 500 search up to 500 then lighter, above 500 search heavier first.
font-style, font-stretchItalic is a separate design; oblique is a slant (default 14deg). Width is matched first and never faked; write font-stretch, since Chrome lacks font-width.
font-synthesisBrowsers fake missing bold (from 600 in Chrome, without widening) and italic (a slant). Turn fakes off where they harm, as for CJK italics, but load real faces where emphasis matters.
The font shorthandSize and family required, family last. Silently resets line-height, every font-variant-*, kerning, font-size-adjust, feature and variation settings. System keywords are legacy. font: inherit for form controls.
font-size-adjustEqualizes x-height across a stack; from-font uses the first font's aspect value. Baseline since July 2024.
Case and underlinestext-transform follows lang (Turkish İ, Greek accents, ß, Dutch IJ). Underlines: thickness, offset, skip-ink and color, as longhands.
People and languagesReaders override fonts through settings, user styles and reader modes; generics carry their choices. Mark lang so CJK text gets the right forms; avoid icon fonts.
This sitePlex stacks end in generics; weights declared per face; strong at 600 with synthesis off, so bold italic, italic headings and italic code fall back to what exists.