All lessons

Lesson 11·Unit 4 · Scripts and layout·Font stacks & glyph coverage·Lv 201·~7 min

CJK font fallback & character coverage

Your 'pure sans-serif' headline silently switches to the OS default for one Hanzi character, and the OS default is different on macOS, Windows, Android, and iOS. A Japanese user gets the Chinese glyph variant. A Chinese user gets the Japanese one. Both are wrong.

fontfallbackOS-specific

By the end

  • ·Explain Han unification: one code point, several regionally correct shapes, and the font picking between them.
  • ·Predict what a browser does when a font lacks a glyph: fall down the stack character by character, landing on an OS default.
  • ·Say what lang="ja" changes for a unified code point, and why a CJK launch needs a font stack per script.

The problem

CJK Unified Ideographs share codepoints across Chinese, Japanese, and Korean, but the canonical glyph shapes differ. If your font stack doesn't include a script-specific CJK font, the browser falls back to the OS default, which often picks the wrong regional variant. Native speakers notice immediately and read it as a sign that you didn't take the locale seriously. Worse: the rendering differs between platforms, so QA on macOS won't catch what an Android user sees.

How it works

A font stack is a negotiation, one glyph at a time

font-family does not mean "use the first font". It means "for each character, use the first font in the list that has a glyph for it". Your Latin headline face almost certainly contains no CJK glyphs. For those characters the browser walks past your whole stack and lands on an OS default, typically a different font on every platform. Nothing errors. One headline quietly mixes two or three fonts, with different metrics on each machine.

The larger issue is Han unification: Unicode assigns one code point to a character that Chinese and Japanese conventions draw differently. 直 (U+76F4) is the classic example. The code point cannot tell the font which shape a reader expects. Only the language can. That is the concrete job of the lang attribute: it steers font selection and glyph conventions for unified code points, and it also feeds line-breaking and assistive tech. Tag the page, and tag every span that switches language.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    On the default mixed sample, compare the “Naive (Helvetica/Arial)” row against the Noto row. The naive stack still renders CJK: the OS silently substituted a system font. Expand the per-font availability list to see which fonts exist on your machine.
  2. 2
    Switch the sample to “Traditional Han characters”. 説 vs 说: watch which stacks render the traditional form with correct glyph conventions, and which fall back to a simplified-leaning font.
  3. 3
    Compare the measured pixel widths listed for each stack. The same string measures differently per stack. Silent fallback shifts layout and truncation behavior per platform, in addition to the letterform change.
  4. 4
    Compare the “N of M fonts available on this device” line across the five stacks. The counts are a property of this machine, not of the page. On another OS the same stacks produce different availability lists, substitutes, and rendering: the bug class that never reproduces on the reporter's machine.

The Han-trad / Han-simp samples look nearly identical in many stacks. Watch for this subtle bug when you ship zh-Hans only.

Naive: Helvetica only0 of 2 fonts available on this device (approximate)
我们在TikTok上工作 (한국어, 日本語, English, 中文) all in one string.

font-family: 'Helvetica Neue', Arial, sans-serif

The stack lists no CJK fonts. The browser falls back to the system 'last-resort' font. The result differs on macOS, Windows, and Android.

Per-font availability check
  • Helvetica Neue
  • Arial
Noto Sans (recommended)0 of 6 fonts available on this device (approximate)
我们在TikTok上工作 (한국어, 日本語, English, 中文) all in one string.

font-family: 'Inter', 'Noto Sans', 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', sans-serif

Google's Noto family covers nearly every Unicode script. Order matters: list the per-language variants (SC/JP/KR/TC) explicitly.

Per-font availability check
  • Inter
  • Noto Sans
  • Noto Sans SC
  • Noto Sans JP
  • Noto Sans KR
  • Noto Sans TC
System Chinese (zh-Hans focus)0 of 5 fonts available on this device (approximate)
我们在TikTok上工作 (한국어, 日本語, English, 中文) all in one string.

font-family: 'Inter', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Source Han Sans SC', sans-serif

This stack picks the correct native font per OS: PingFang on macOS, Microsoft YaHei on Windows.

Per-font availability check
  • Inter
  • PingFang SC
  • Hiragino Sans GB
  • Microsoft YaHei
  • Source Han Sans SC
System Japanese0 of 5 fonts available on this device (approximate)
我们在TikTok上工作 (한국어, 日本語, English, 中文) all in one string.

font-family: 'Inter', 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', 'Yu Gothic UI', 'Meiryo', sans-serif

Japanese-first stack. Traditional or simplified Chinese characters that render here use Japanese glyph variants, subtly wrong.

Per-font availability check
  • Inter
  • Hiragino Kaku Gothic ProN
  • Hiragino Sans
  • Yu Gothic UI
  • Meiryo
Serif (Source Serif + Noto Serif CJK)0 of 5 fonts available on this device (approximate)
我们在TikTok上工作 (한국어, 日本語, English, 中文) all in one string.

font-family: 'Source Serif 4', 'Noto Serif', 'Noto Serif SC', 'Noto Serif JP', 'Noto Serif KR', serif

Editorial style for long-form blog content in mixed scripts.

Per-font availability check
  • Source Serif 4
  • Noto Serif
  • Noto Serif SC
  • Noto Serif JP
  • Noto Serif KR

Fallback happens per character

CSS fonts cascade per character, not per element. Suppose your stack lists Helvetica first and a header contains one Chinese character. That character silently renders in whatever font the OS picks, often a serif that breaks the visual hierarchy.

The fix: list a CJK family directly after your primary sans-serif. Use font-language-override or per-language CSS (:lang(ja), :lang(zh-Hans)) when you mix simplified, traditional, and Japanese Kanji on the same page. Otherwise the OS picks Han-unification glyph variants that look wrong in the user's target.

If you remember one thing

Declare lang and ship an explicit CJK font stack. When you do not, the OS silently substitutes a default font: a different one per platform, with the wrong glyph variants.

What to do about it

  • Bundle a Noto-class CJK webfont (Noto Sans SC, Noto Sans JP, Noto Sans KR) with the correct script subset for the locale you're shipping.
  • Set the lang attribute on elements containing CJK text so the browser can pick the correct glyph variant from a shared font and trigger the right OS fallback.
  • Define separate CSS font-family stacks per locale (e.g. .font-cjk-zh, .font-cjk-ja) and apply via html[lang] selectors or component-level classes.
  • Always QA in the target locale's primary OS: Windows for SC, macOS & iOS for JP/TC, Android for KR. A native speaker testing on the wrong OS misses half the bugs.

Use this with

Stakeholders

EngineeringDesignQA

Moments

  • ·Launch prep for a CJK market
  • ·Brand audit / design system review
  • ·Font-license procurement conversation with finance

Field note

Han unification gives Chinese, Japanese, and (historically) Korean the same code points for thousands of characters: U+76F4 直 is one code point, but its expected shape differs between Chinese and Japanese conventions. The font decides which shape your user sees, and your stack, the lang attribute, and the OS decide the font. That a CJK string renders proves nothing. Check whether it renders with the shapes this market expects.

Wikipedia: Han unification

Quick check

3 questions · pass at 2+

  1. Question 1/3

    What is “Han unification” in Unicode?

  2. Question 2/3

    The first font in your font-family lacks a glyph the text needs. What does the browser do?

  3. Question 3/3

    Why does <html lang="ja"> (or lang on the element) matter for CJK rendering?

Words you'll hear

Where to read more

Related lessons