CSS unicode-range Descriptor
The unicode-range descriptor in @font-face rules specifies which Unicode characters a particular font resource should be applied to. This enables the browser to load only the font files needed for the characters actually present on the page, significantly reducing unnecessary font downloads.
Basic Syntax
@font-face {
font-family: 'MyFont';
src: url('myfont-latin.woff2') format('woff2');
unicode-range: U+0000-00FF; /* Basic Latin and Latin-1 Supplement */
}
@font-face {
font-family: 'MyFont';
src: url('myfont-greek.woff2') format('woff2');
unicode-range: U+0370-03FF; /* Greek and Coptic */
}
@font-face {
font-family: 'MyFont';
src: url('myfont-cyrillic.woff2') format('woff2');
unicode-range: U+0400-04FF; /* Cyrillic */
}
If a page only contains Latin text, the Greek and Cyrillic font files are never downloaded.
Range Syntax
/* Single code point */
unicode-range: U+26; /* & (AMPERSAND) */
/* Range */
unicode-range: U+0025-00FF; /* % through ÿ */
/* Wildcard (? matches any hex digit) */
unicode-range: U+4??; /* U+0400 through U+04FF */
/* Multiple values */
unicode-range: U+0025-00FF, U+4??, U+FF00-FF9F;
Google Fonts and unicode-range
Google Fonts uses unicode-range extensively to split fonts into script-specific subsets. When you load a Google Font, the CSS it returns contains multiple @font-face declarations, each covering a different script:
/* Cyrillic subset */
@font-face {
font-family: 'Roboto';
src: url(roboto-cyrillic.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* Latin subset */
@font-face {
font-family: 'Roboto';
src: url(roboto-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, ...;
}
The browser downloads only the font files needed for the detected characters, making multilingual pages efficient.
Performance Impact
unicode-range is one of the most impactful web font optimization techniques:
- A full CJK font can be 5-15 MB; a Latin subset might be 20-50 KB
- By specifying ranges, a Japanese-primary site with occasional Latin text downloads the Japanese file and a small Latin supplement
- The browser performs font matching per character, not per text run
Creating Subset Fonts
The pyftsubset tool from the fonttools package creates subset font files for specific Unicode ranges:
pip install fonttools brotli
# Create a Latin-only subset
pyftsubset input.ttf \
--unicodes="U+0000-00FF,U+0131,U+0152-0153" \
--flavor=woff2 \
--output-file=font-latin.woff2
Browser Support and Fallback
unicode-range has excellent support across all modern browsers (Chrome, Firefox, Safari, Edge). In the absence of a matching @font-face rule for a character, the browser falls back to the system font stack, so the worst case is an unstyled character rather than a missing one.
Use Case: Emoji Font Supplementation
/* Use a custom font for all characters except emoji */
body {
font-family: 'MyFont', sans-serif;
}
/* Keep system emoji rendering for emoji code points */
@font-face {
font-family: 'MyFont';
src: url('myfont.woff2') format('woff2');
/* Exclude emoji ranges so system emoji font takes over */
unicode-range:
U+0000-1F3FF,
U+1F500-1FFFF, /* Gaps between emoji blocks */
U+2000-2BFF; /* Misc symbols and arrows */
}