Font fallback is the mechanism by which operating systems, browsers, and text rendering engines substitute an alternative font when the primary font specified for a piece of text does not contain a glyph for a particular character. Without font fallback, missing characters would uniformly display as blank rectangles or question marks. With it, the system silently substitutes from a chain of fallback fonts until a matching glyph is found.
How Fallback Works
When rendering a string, the shaping engine checks each character against the active font's character map (cmap table). If a glyph is found, it renders normally. If not, the engine moves through the font fallback stack:
- Fonts explicitly listed in the CSS
font-familyproperty - System-defined generic font families (
serif,sans-serif,monospace) - Operating system fallback fonts (platform-specific)
- The special "Last Resort" font (Unicode fallback of last resort, shows .notdef glyphs)
/* Explicit fallback chain */
body {
font-family:
'Inter', /* preferred */
'Helvetica Neue', /* macOS fallback */
'Arial', /* Windows fallback */
'Noto Sans', /* broad Unicode coverage */
sans-serif; /* system generic */
}
/* For CJK content, specify CJK fallbacks explicitly */
.cjk {
font-family:
'Noto Sans CJK JP',
'Yu Gothic', /* macOS/Windows Japanese */
'Hiragino Sans', /* macOS Japanese */
'Meiryo', /* Windows Japanese */
sans-serif;
}
System Fallback Fonts by Platform
macOS:
Latin: San Francisco (SF Pro)
CJK: Hiragino Sans / Hiragino Mincho
Arabic: Geeza Pro
Hebrew: Arial Hebrew
Thai: Thonburi
Windows:
Latin: Segoe UI
CJK: Yu Gothic (Japanese), Malgun Gothic (Korean), Microsoft JhengHei (Chinese)
Android:
Latin: Roboto
CJK: Noto Sans CJK
Everything else: Noto family
Linux:
Varies by distribution; commonly DejaVu, Liberation, or Noto
Unicode Ranges for Subsetting
CSS @font-face with unicode-range allows precise control over when a font is used, enabling efficient fallback without loading unnecessary font files:
/* Load Latin characters from one font, CJK from another */
@font-face {
font-family: 'MyFont';
src: url('myfont-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}
@font-face {
font-family: 'MyFont';
src: url('noto-sans-cjk.woff2') format('woff2');
unicode-range: U+4E00-9FFF, U+3400-4DBF; /* CJK Unified Ideographs */
}
Detecting Font Fallback in JavaScript
// Check if a font covers a specific character
// using the FontFaceSet.check() API
document.fonts.check('16px Inter', '\u8A9E');
// returns false if 'Inter' does not cover this character
// Canvas-based detection: compare rendering metrics
function isUsingFallback(char, preferredFont) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `16px "${preferredFont}"`;
const withFont = ctx.measureText(char).width;
ctx.font = '16px monospace'; // neutral fallback
const withFallback = ctx.measureText(char).width;
// Significant width difference suggests fallback was used
return Math.abs(withFont - withFallback) > 1;
}
Noto: Designed for Fallback
Google's Noto font family is designed specifically to provide comprehensive Unicode coverage as a fallback font. The name "Noto" comes from "no tofu" -- eliminating the blank rectangle placeholder. The Noto family covers over 1,000 languages and all Unicode scripts, making it the standard recommendation for global fallback coverage.
Performance Considerations
Every font in the fallback chain that is loaded over the network adds to page weight. Use unicode-range in @font-face declarations to ensure that fallback fonts are only downloaded when the page actually contains characters that require them, rather than loading speculatively.