How to whitelist builder edge cases and common mistakes
- Step 1Confirm coverage before you whitelist — The tool silently maps unmatched characters to `.notdef`. Run the [Character Coverage Map](/font-tools/character-coverage-map) (it scores against 346 real Unicode blocks) to confirm the font has every glyph you intend to keep — accents, symbols, and non-Latin scripts are the usual gaps.
- Step 2Paste exactly the characters you need — and nothing speculative — The single 'Characters to keep' textarea is decoded codepoint-by-codepoint. Include spaces and punctuation explicitly. Duplicates are harmless (deduped by codepoint). The whole box is trimmed at its edges, but inner whitespace is kept.
- Step 3Expect a TTF, and plan the WOFF2 step — Output is `<stem>.subset.ttf`, uncompressed. The size in the result panel is the raw TTF; convert with [TTF→WOFF2](/font-tools/ttf-to-woff2) for the real web size. There is no format option in this tool.
- Step 4Accept that kerning and features are gone — The result panel states 'Kerning + OT layout features dropped; outlines preserved.' If your text relies on `kern`/`liga`/`ss0x`, the subset will look different — preview it, and use a layout-preserving pipeline if it matters.
- Step 5Read the error if the build fails — Two errors are possible: 'Paste at least one character to keep' (empty box) and 'Subset would be empty — no glyphs in this font match the requested charset' (zero matches). A third can appear on output: 'Subsetting failed in opentype.js's font writer' for CFF/OTF fonts.
- Step 6Choose a different tool when whitelisting doesn't fit — Variable text → [Font Subsetter](/font-tools/font-subsetter) charset. Need kerning → layout-preserving pipeline. Just inspecting glyphs → [Glyph Inspector](/font-tools/glyph-inspector) or [Glyph Count Analyzer](/font-tools/glyph-count-analyzer). Removing only emoji → [Emoji Remover](/font-tools/emoji-remover).
Whitelist Builder behaviour matrix
What the tool does with each kind of input, grounded in the implementation. 'Silent' means no error — you only notice when the rendered text shows boxes.
| Input / situation | What happens | Error or silent? |
|---|---|---|
| Empty 'Characters to keep' box | Throws 'Paste at least one character to keep.' | Error |
| Characters that match no glyph in the font | Those codepoints aren't kept; if none match, throws 'Subset would be empty' | Silent per-char; error only if zero total |
Duplicate characters (APPS) | Deduped by codepoint — one glyph each | Silent (no effect) |
| Leading/trailing whitespace around the box | Trimmed off the whole input | Silent |
| Inner space between words | Kept as glyph for U+0020 | Silent (correct) |
Precomposed é (U+00E9) | Kept as one glyph if the font has it | Silent |
e + combining acute (U+0301) | Both codepoints matched separately; positioning may be off (GPOS dropped) | Silent |
| ZWJ emoji sequence (👨👩👧) | Split into base codepoints + ZWJ; kept individually, sequence won't compose | Silent |
| OTF with CFF (PostScript) outlines | Writer may throw 'Subsetting failed in opentype.js's font writer' | Error (on output) |
What the rebuild keeps vs drops
The subset is a fresh font built from new Font({ glyphs }). Only the listed properties carry over; the rest of the original font's tables do not.
| Font feature | In the subset? | Consequence |
|---|---|---|
.notdef (glyph 0) | Always kept | Missing-glyph box renders correctly; spec-compliant |
| Kept glyph outlines | Yes, byte-exact | Letterforms are identical to the source |
| Units-per-em, ascender, descender | Yes | Metrics and proportion preserved |
kern / GPOS | Dropped | No pair kerning, no mark positioning |
GSUB | Dropped | No ligatures, no stylistic sets, no contextual swaps |
Hinting (fpgm/prep/cvt) | Not carried as program tables | Rendering at small sizes may differ; usually fine for display |
Full name table | Family name kept; rest not faithful | License still applies to the subset |
| Output format | TTF (uncompressed) | Run TTF→WOFF2 before shipping |
Cookbook
Reproductions of the behaviours teams trip over, with the exact input and the observed result. Use these to verify your expectations before you build.
Partial miss is silent — accented name tofus
ExampleThe tool only errors when ZERO characters match. A single missing accent quietly becomes .notdef, so the bug shows up only in the rendered page.
Font: a caps-only display face without accents Characters to keep: CRÉME Result: C, R, M, E kept; É (U+00C9) has no glyph -> not kept. No error (4 of 5 matched). Rendered 'CRÉME' shows a box for É. Fix: confirm coverage first via /font-tools/character-coverage-map
Zero match — hard error
ExampleWhitelisting Cyrillic from a Latin-only font matches nothing and errors. This is the one case the tool refuses outright.
Font: Latin-only
Characters to keep: Привет
Error: "Subset would be empty — no glyphs in this font match
the requested charset (6 chars). Run the Character
Coverage Map tool first to confirm coverage."Ligature splits into separate letters
ExampleGSUB is dropped, so a font that renders 'fi' as a single ligature glyph via the liga feature will instead show f and i separately in the subset.
Source renders: "office" with an fi ligature (via liga / GSUB) Characters to keep: office Subset: keeps f, i, c, e, o glyphs; GSUB dropped. Rendered: f and i sit as separate glyphs (no ligature). If the ligature look matters -> layout-preserving pipeline.
ZWJ emoji sequence doesn't compose
ExampleMulti-codepoint emoji are decoded into their component codepoints. Without GSUB and the right glyphs, the family emoji won't render as one image.
Characters to keep: 👨👩👧 (man+ZWJ+woman+ZWJ+girl)
Decoded codepoints: U+1F468, U+200D (ZWJ), U+1F469,
U+200D, U+1F467
Kept individually if the font has those base glyphs;
the composed family glyph (a GSUB substitution) will NOT
appear. Use /font-tools/emoji-remover for emoji-font work.CFF/OTF writer failure
Exampleopentype.js can parse a CFF OTF but its writer may fail to serialize the subset. The error is explicit; the fix is to convert to TrueType outlines first.
Font: Brand-Display.otf (CFF / PostScript outlines)
Characters to keep: BRAND
Error: "Subsetting failed in opentype.js's font writer (...).
This typically affects fonts with CFF (PostScript)
outlines or non-standard tables."
Fix: convert OTF -> TTF (TrueType outlines) first, then subset.Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Output is TTF, not WOFF2
By designEvery run produces <stem>.subset.ttf — uncompressed TrueType — because opentype.js's Font.toArrayBuffer() writes sfnt. There is no WOFF2 option in the UI. The well-circulated '~1.5–3 KB' figure is the size after converting via TTF→WOFF2. Shipping the raw .subset.ttf to the web wastes 2–3× the bytes.
Kerning is always dropped
Kerning droppedThe subset is rebuilt from new Font({ glyphs }), which carries outlines and basic metrics but not kern/GPOS. The result panel says so explicitly. There is no option to preserve kerning in this tool — it's a property of the in-browser engine. If pair-kerning matters, use pyftsubset --layout-features='*' or hb-subset in a pipeline (see the pipeline guide).
Ligatures and stylistic sets vanish
OT features droppedGSUB is dropped along with GPOS, so ligatures (liga/dlig), stylistic sets (ss01…), small caps (smcp), and oldstyle/tabular figures (onum/tnum) won't apply in the subset — you get the default glyphs only. If your text's appearance depends on an OpenType feature, whitelisting will change it. Bake the look into the design or keep a layout-preserving engine.
Partial misses are silent (.notdef)
SilentIf some — but not all — of your characters lack glyphs in the font, those codepoints are simply not kept, with no warning. The rendered text shows .notdef boxes for them. The tool errors only when zero characters match. Always verify coverage with the Character Coverage Map before relying on the subset, especially for accents and symbols.
Empty input or zero matches
ErrorAn empty 'Characters to keep' box throws 'Paste at least one character to keep.' A non-empty box where none of the codepoints exist in the font throws 'Subset would be empty — no glyphs in this font match the requested charset.' Both are clear, hard failures — you won't get a silently-broken file in these two cases.
Combining marks and decomposed clusters
Split into codepointsInput is decoded with Array.from + codePointAt, so a base letter plus a combining mark (e.g. e + U+0301) becomes two separate codepoints. Both must be in the font, and because GPOS (mark positioning) is dropped, the accent may not sit correctly over the base. Prefer precomposed characters (é = U+00E9) in fixed text to sidestep this.
ZWJ / multi-codepoint emoji don't compose
Split into codepointsAn emoji ZWJ sequence (family, profession, flag) is decoded into its component codepoints plus the ZWJ (U+200D). The composed image is a GSUB substitution, which is dropped, so the sequence won't render as one glyph even if the base glyphs are kept. For emoji-font work, use the Emoji Remover or a dedicated emoji subsetter instead.
CFF / OTF (PostScript) font fails on write
Writer erroropentype.js can parse CFF OTFs but its writer can throw when serializing the subset, surfacing 'Subsetting failed in opentype.js's font writer.' The fix is to convert the OTF to a TrueType-outline TTF before subsetting, or use pyftsubset/hb-subset, which handle CFF and CFF2 correctly.
Hinting differences at small sizes
Mostly fineThe rebuild does not carry the font's hinting program tables (fpgm/prep/cvt) as a control program, so small-size rendering can differ slightly from the original on some platforms. Because whitelist subsets are almost always used for large display text (logos, heroes), this rarely matters. If you do need the smallest file, run the Hinting Stripper deliberately.
License travels with the subset
Carried overSubsetting changes the bytes, not the license. The subset's family name is carried from the source's English fontFamily; the rest of the name/license metadata isn't faithfully reproduced, but the original EULA still governs the subset. Confirm the font's license permits web embedding before shipping. For name-table size reduction on a full font, use the Name Table Cleaner.
Frequently asked questions
Why does my whitelisted text show empty boxes?
Those boxes are .notdef — the font has no glyph for a codepoint you pasted, so it wasn't kept. The tool only errors when none of your characters match; partial misses are silent. The usual culprits are accents, typographic punctuation (em dash, curly quotes), currency symbols, and non-Latin glyphs the font doesn't contain. Run the Character Coverage Map first to confirm the font has everything you need.
Why doesn't the subset keep kerning?
Because the tool rebuilds the font from scratch with opentype.js (new Font({ glyphs })), which carries outlines and basic metrics but not the kern/GPOS tables. The result panel states 'Kerning + OT layout features dropped; outlines preserved.' There's no option to keep kerning here — it's inherent to the in-browser engine. For kerning-preserving output, use pyftsubset --layout-features='*' or hb-subset in a build pipeline.
Why is the output a TTF instead of WOFF2?
opentype.js's writer produces sfnt/TTF; the tool has no WOFF2 writer. So you always get <stem>.subset.ttf, uncompressed. To reach the small web sizes people quote (~1.5–3 KB for a logo), run the TTF through TTF→WOFF2 as a second step. Don't ship the raw TTF to production — it's 2–3× larger than its WOFF2.
What happens to ligatures and stylistic sets?
They're dropped. GSUB (which drives ligatures, stylistic sets, small caps, and figure-style features) is not carried into the subset, so the rebuilt font renders only the default base glyphs. If your text's appearance depends on an OpenType feature, whitelisting will change how it looks. Either keep the feature-bearing font, or have the foundry supply the styled glyph as a single drawn glyph you can whitelist directly.
How does it handle emoji and combining characters?
It decodes input into individual codepoints (codePointAt), so a ZWJ emoji sequence or a base + combining-mark cluster is split into its components. Each component is kept only if the font has a glyph for it, and because GPOS/GSUB are dropped, mark positioning and emoji composition won't work. Precomposed characters (like é = U+00E9) work as single glyphs; composed sequences don't. For emoji fonts, use the Emoji Remover.
When does the tool actually throw an error?
Three cases. (1) Empty 'Characters to keep' box → 'Paste at least one character to keep.' (2) None of your characters exist in the font → 'Subset would be empty — no glyphs in this font match the requested charset.' (3) On output, a CFF/OTF font can trip the writer → 'Subsetting failed in opentype.js's font writer.' Everything else — including partial coverage misses — succeeds without warning.
Why did my OTF font fail to process?
Likely it has CFF (PostScript) outlines. opentype.js can parse CFF OTFs but its writer can fail when serializing the subset, giving 'Subsetting failed in opentype.js's font writer.' Convert the OTF to a TrueType-outline TTF in a desktop tool first, then subset — or use pyftsubset/hb-subset in a pipeline, which handle CFF and CFF2 properly.
Are duplicate characters or extra whitespace a problem?
No. The whitelist is a Set of codepoints, so duplicate characters (the two P's in 'APPS') collapse to one glyph with no ill effect. Leading and trailing whitespace around the whole input is trimmed automatically; inner spaces are kept as real U+0020 glyphs (so 'JAD APPS' keeps its space). You can paste messy input and the codepoint extraction cleans it up.
Does .notdef always get included?
Yes — glyph 0 (.notdef) is pushed into the subset unconditionally, as the OpenType spec requires. That's why the result panel's 'Glyphs kept' is always your unique-character count plus one. It's what renders the missing-glyph box for any character outside your whitelist, and it keeps the font spec-valid.
What input file sizes and tiers are supported?
It's a Pro-tier tool (minTier: pro). On Pro the font-file limit is 50 MB (Developer raises it to 1 GB), and the glyph limit is 65,536 — far above anything a whitelist subset needs, since you're keeping a handful of glyphs. Input formats are TTF, OTF, WOFF, and WOFF2.
When should I NOT use the Whitelist Builder?
When the text can vary (use a Font Subsetter charset so unexpected characters still render); when kerning or OpenType features are essential (use a layout-preserving pipeline); when you only want to inspect glyphs rather than cut a font (use the Glyph Inspector or Glyph Count Analyzer); or when you only need to strip emoji (use the Emoji Remover).
Is the subset still covered by the original font license?
Yes. Subsetting changes the bytes but not the legal terms — the subset is governed by the source font's EULA, which often restricts web embedding and redistribution. The subset carries the original family name; the rest of the name table isn't faithfully reproduced. Always confirm the license permits self-hosted web use before shipping a subset, especially for commercial foundry fonts.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.