Skip to content

Commit fd30c65

Browse files
committed
Add font-family post
1 parent cd22c97 commit fd30c65

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
---
2+
layout: post
3+
title: "font-family Doesn’t Fall Back the Way You Think"
4+
date: 2026-04-10 11:30:00
5+
categories: Web Development
6+
main: ""
7+
meta: "A quick but important reminder that font-family declarations don’t inherit fallback stacks the way many developers assume."
8+
---
9+
10+
There is a small but surprisingly important nuance in the way `font-family`
11+
works that seems to catch a lot of people out. In my [continuing
12+
series](/2026/04/what-is-css-containment-and-how-can-i-use-it/) on [web
13+
performance for design
14+
systems](/2026/03/when-all-you-can-do-is-all-or-nothing-do-nothing/), today
15+
we’ll look at font stacks and how, when improperly configured, they can cause
16+
unsightly flashes of inappropriate or unexpected fallback text, and in more
17+
extreme cases, layout shifts.
18+
19+
Correctly, developers for the most part know that `font-family` is an inherited
20+
property: set a font family on the `:root`/`html`/`body` and, unless told
21+
otherwise, descendants will inherit that font:
22+
23+
```css
24+
body {
25+
font-family: system-ui, sans-serif;
26+
}
27+
```
28+
29+
So far, so good!
30+
31+
The confusion tends to arrive when we introduce a web or custom font on a child
32+
element, e.g.:
33+
34+
```css
35+
h1 {
36+
font-family: "Open Sans";
37+
}
38+
```
39+
40+
At a glance, this can feel perfectly sensible. The page should use `system-ui,
41+
sans-serif`; the heading uses `"Open Sans"`; and while the web font is loading,
42+
the browser will presumably just fall back to the parent’s stack—`system-ui,
43+
sans-serif`.
44+
45+
Unfortunately, that isn’t the case.
46+
47+
## `font-family` Fallbacks Are Self-Contained
48+
49+
Once you declare a `font-family` on an element, that declaration stands on its
50+
own. The element does not say: <q>I would like `"Open Sans"`, and if that is
51+
unavailable right now, please work your way back up the DOM and inherit whatever
52+
fallbacks the nearest ancestor might have.</q>
53+
54+
Instead, it says: <q>My `font-family` is `"Open Sans"`.</q> And that’s all it
55+
says.
56+
57+
And if the browser does not yet have `"Open Sans"` available (yet), it resolves
58+
fallback from _that declaration_, not from the parent’s.
59+
60+
Put another way:
61+
62+
```css
63+
h1 {
64+
font-family: "Open Sans"; /* « The fallback happens inside here… */
65+
}
66+
67+
/**
68+
* …not here.
69+
*/
70+
body {
71+
font-family: system-ui, sans-serif;
72+
}
73+
```
74+
75+
## Why You Get a Flash of Times
76+
77+
If the current element’s `font-family` declaration contains only one value, and
78+
that value is not currently available, the browser falls back to its default
79+
**for that element**, and not to an inheritable `font-family` from somewhere
80+
higher up. For most browsers in their default state, that fallback is likely
81+
_Times_ or _Times New Roman_. That is why you so often see a brief flash of
82+
Times New Roman where you were expecting something much more sympathetic or
83+
appropriate.
84+
85+
The browser is not _forgetting_ the parent’s font stack; it’s obeying the
86+
child’s declaration exactly as written, then exhausting the options available in
87+
that declaration, and then falling back to the browser default.
88+
89+
## The Fix Is Simple
90+
91+
Whenever you specify a `font-family`, specify a **complete stack**. I’m looking
92+
at a client’s site right now and I can see this right at the very top of their
93+
CSS:
94+
95+
```css
96+
:root {
97+
--hero-hero: "Clan Pro";
98+
--heading-x-large: "Clan Pro";
99+
--heading-large: "Clan Pro";
100+
--heading-medium: "Clan Pro";
101+
--heading-medium-subtle: "Clan Pro";
102+
--heading-small: "Clan Pro";
103+
--heading-small-subtle: "Clan Pro";
104+
--heading-x-small: "Clan Pro";
105+
--paragraph-title-x-large: "Bernina Sans";
106+
--paragraph-title-large: "Bernina Sans";
107+
--paragraph-title-medium: "Bernina Sans";
108+
--paragraph-title-small: "Bernina Sans";
109+
--paragraph-title-x-small: "Bernina Sans";
110+
--paragraph-body-x-large: "Bernina Sans";
111+
--paragraph-body-large: "Bernina Sans";
112+
--paragraph-body-medium: "Bernina Sans";
113+
--paragraph-body-small: "Bernina Sans";
114+
--paragraph-body-x-small: "Bernina Sans";
115+
--label-3-x-large: "Clan Pro";
116+
--label-2-x-large: "Clan Pro";
117+
--label-x-large: "Clan Pro";
118+
--label-large: "Clan Pro";
119+
--label-medium: "Clan Pro";
120+
--label-small: "Clan Pro";
121+
--label-x-small: "Clan Pro";
122+
}
123+
```
124+
125+
At the very least, all of these simply need to read:
126+
127+
```css
128+
:root {
129+
--hero-hero: "Clan Pro", sans-serif;
130+
--heading-x-large: "Clan Pro", sans-serif;
131+
--heading-large: "Clan Pro", sans-serif;
132+
--heading-medium: "Clan Pro", sans-serif;
133+
--heading-medium-subtle: "Clan Pro", sans-serif;
134+
--heading-small: "Clan Pro", sans-serif;
135+
--heading-small-subtle: "Clan Pro", sans-serif;
136+
--heading-x-small: "Clan Pro", sans-serif;
137+
--paragraph-title-x-large: "Bernina Sans", sans-serif;
138+
--paragraph-title-large: "Bernina Sans", sans-serif;
139+
--paragraph-title-medium: "Bernina Sans", sans-serif;
140+
--paragraph-title-small: "Bernina Sans", sans-serif;
141+
--paragraph-title-x-small: "Bernina Sans", sans-serif;
142+
--paragraph-body-x-large: "Bernina Sans", sans-serif;
143+
--paragraph-body-large: "Bernina Sans", sans-serif;
144+
--paragraph-body-medium: "Bernina Sans", sans-serif;
145+
--paragraph-body-small: "Bernina Sans", sans-serif;
146+
--paragraph-body-x-small: "Bernina Sans", sans-serif;
147+
--label-3-x-large: "Clan Pro", sans-serif;
148+
--label-2-x-large: "Clan Pro", sans-serif;
149+
--label-x-large: "Clan Pro", sans-serif;
150+
--label-large: "Clan Pro", sans-serif;
151+
--label-medium: "Clan Pro", sans-serif;
152+
--label-small: "Clan Pro", sans-serif;
153+
--label-x-small: "Clan Pro", sans-serif;
154+
}
155+
```
156+
157+
Remember, any time you declare a `font-family`, declare the whole thing. Even if
158+
that is just a broad
159+
[`<generic-family>`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/generic-family)
160+
And while this is the bare minimum, at least sans-serif web fonts will actually fall
161+
back to sans.
162+
163+
To do a much more thorough job, you can simply [hire me](/contact/) to run my
164+
<cite>Web Performance for Design Systems</cite> workshop.
165+
166+
## Why This Matters
167+
168+
At its most simple, this is a trivial visual issue: a nascent sans heading
169+
briefly rendered in serif just looks wrong.
170+
171+
At the other end of the spectrum, it can have real knock-on effects on Core Web
172+
Vitals: if the fallback face is excessively different in width, height, or
173+
overall proportions, the eventual swap to the web font can have an impact on
174+
your CLS scores.
175+
176+
## Closing Thoughts
177+
178+
If a `font-family` matters enough to override, it matters enough to define
179+
properly. This is one of those small details that feels too small to matter
180+
right up until you notice it everywhere.
181+
182+
My client has had complaints of noticeable layout shifts while migrating to
183+
a new design system, and at the size and scale they’re working at, they were
184+
really, really struggling to pin it down. It only took me a few minutes because
185+
_it’s easy when you know the answer_. That’s exactly why you [hire
186+
consultants](/consultancy/).

0 commit comments

Comments
 (0)