CSS unicode-range Explained for Font Subsetting

The CSS @font-face descriptor unicode-range allows web developers to split large font files into smaller, modular chunks and instruct the browser to download only the specific character sets needed by a webpage. This article explains how unicode-range functions, why it is critical for optimizing large character sets like Chinese, Japanese, Korean (CJK), or multilingual typefaces, and how it dramatically improves page loading performance by avoiding unnecessary font downloads.

The Problem with Large Character Sets

Standard Latin-based fonts usually contain a few hundred glyphs, resulting in file sizes of roughly 20 KB to 100 KB. In contrast, fonts supporting languages such as Chinese, Japanese, Korean, or extensive international and technical symbols can contain tens of thousands of glyphs. These full-scale font files often exceed 5 MB to 20 MB.

Forcing a browser to download a multi-megabyte font file causes significant latency, delays text rendering, triggers high Cumulative Layout Shift (CLS), and wastes mobile bandwidth—especially if the webpage only displays a few dozen unique characters from that set.

How unicode-range Solves This

The unicode-range descriptor defines a specific range of Unicode code points that a particular @font-face resource covers.

When you define a unicode-range, modern browsers do not download the font file immediately upon discovering the CSS rule. Instead, the browser scans the text content of the page. If, and only if, a character on the page falls within the declared Unicode range, the browser requests that specific font file from the server.

Implementation and Subsetting

To leverage unicode-range effectively, a large master font file must first be split into smaller subsets (using tools like pyftsubset or font optimization pipelines). Once partitioned, each subset is assigned to the same font-family name, but with a unique unicode-range.

/* Latin Character Subset */
@font-face {
  font-family: 'OpenSansComposite';
  src: url('fonts/OpenSans-Latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

/* Cyrillic Character Subset */
@font-face {
  font-family: 'OpenSansComposite';
  src: url('fonts/OpenSans-Cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491;
}

/* High-frequency CJK Subset */
@font-face {
  font-family: 'OpenSansComposite';
  src: url('fonts/NotoSans-CJK-Subset1.woff2') format('woff2');
  unicode-range: U+4E00-51FF;
}

body {
  font-family: 'OpenSansComposite', sans-serif;
}

Syntax Format for Code Points

The descriptor accepts single values, ranges, and wildcards separated by commas:

  • Single code point: U+26 (the & character)
  • Contiguous range: U+0020-007F (standard ASCII range)
  • Wildcard range: U+4??? (covers all code points from U+4000 to U+4FFF)
  • Multiple ranges: U+0000-00FF, U+0131, U+0152-0153

Key Performance Benefits

  • Zero Waste: If a page only contains English text, the Cyrillic or CJK font chunks mapped under the same family are never fetched.
  • Faster Largest Contentful Paint (LCP): Critical initial text renders immediately with smaller initial subsets instead of waiting for a monolithic file to download.
  • Selective Overrides: Developers can use custom decorative fonts for specific characters (such as ampersands or copyright symbols) while letting system or body fonts handle the rest, without adding separate HTML classes to those characters.