How to build a hyper-minimal logo font from a character whitelist
- Step 1Get your brand font as TTF, OTF, WOFF, or WOFF2 — All four formats are accepted (`.ttf,.otf,.woff,.woff2`). WOFF and WOFF2 are decompressed to a raw sfnt internally before subsetting, so there is no need to convert first. The tool requires a **Pro tier** or above (it is a `minTier: pro` tool); the font-file limit on Pro is 50 MB, which covers any logo font.
- Step 2Drop the font onto the tool above — Parsing happens in-browser with opentype.js. The font never leaves your machine — important when the wordmark uses a licensed display face you cannot redistribute.
- Step 3Paste your exact wordmark into 'Characters to keep' — The single textarea (placeholder `JAD APPS 0123456789`) is the only control. Type the literal characters of your logo. Whitespace at the very start/end of the box is trimmed, but a space **between** words is a real codepoint (U+0020) and is kept if you include it. Duplicate letters collapse automatically — "APPS" contributes one `P`, not two.
- Step 4Include every glyph the mark actually renders — Caps wordmark? Paste only the caps you use — lowercase glyphs are dropped. Mixed case, an ampersand, a registered-trademark `®`, an em dash in a tagline — each must appear in the box or it becomes `.notdef`. Verify the source font even contains them first with the [Character Coverage Map](/font-tools/character-coverage-map).
- Step 5Download the subset TTF — Output is `<stem>.subset.ttf` — an uncompressed TrueType file. The result panel reports glyphs-in-source, glyphs-kept (your count + 1 for `.notdef`), source KB, and subset KB. The note line is explicit: "Kerning + OT layout features dropped; outlines preserved."
- Step 6Compress to WOFF2 before shipping — A raw TTF is the wrong format for the web. Pass the subset through [TTF→WOFF2](/font-tools/ttf-to-woff2) to get the ~1.5–3 KB hero-font size, then wire it into an `@font-face` with the [Font-Face Generator](/font-tools/font-face-generator).
Where the bytes go: a 'JAD APPS' wordmark
Worked example for a 6-unique-glyph caps wordmark (J A D P S + space). Sizes are representative of a typical 200 KB display TTF; your numbers vary with outline complexity and units-per-em. The tool emits the TTF column; the WOFF2 column is what you get after the separate converter step.
| Stage | Format | Glyphs | Approx size | How |
|---|---|---|---|---|
| Source font | TTF / OTF / WOFF2 | ~250 | ~80–250 KB | What the foundry shipped |
| After whitelist | TTF (this tool) | 7 (6 + .notdef) | ~4–8 KB | Paste JAD APPS → download <name>.subset.ttf |
| After WOFF2 | WOFF2 | 7 | ~1.5–3 KB | Run the TTF through TTF→WOFF2 |
| Gzip on the wire | WOFF2 (already brotli'd) | 7 | ~1.5–3 KB | WOFF2 is pre-compressed; HTTP gzip barely changes it |
What 'Characters to keep' actually does with your input
The textarea is the only option. Behaviour is codepoint-exact: each character you paste is decoded to a Unicode codepoint and matched against the font's glyph coverage.
| You paste | Codepoints kept | Result |
|---|---|---|
JAD APPS | J, A, D, space, P, S (6 unique) | 7 glyphs out (6 + .notdef); the second P is a duplicate codepoint, ignored |
JADAPPS (no space) | J, A, D, P, S (5 unique) | 6 glyphs out; space (U+0020) is not kept — a CSS word-spacing won't render a missing space glyph |
Acme® | A, c, m, e, ® (U+00AE) | Kept only if the source font has a ® glyph; if not, ® falls to .notdef |
café | c, a, f, é (U+00E9) | Precomposed é kept as one glyph. If your text used e + U+0301 combining accent instead, paste both — they're separate codepoints |
JAD (padded) | J, A, D | Leading/trailing whitespace is trimmed off the whole box; inner characters survive |
| (empty box) | — | Error: "Paste at least one character to keep." |
Cookbook
Copy-paste-ready whitelists and the compress-then-embed chain for real wordmarks. The tool's only input is the character string; everything else is the downstream WOFF2 + @font-face step.
An all-caps wordmark, minimal whitelist
ExampleA caps logo needs only the uppercase letters that appear plus a space. Lowercase, digits, and punctuation are all dropped — paste nothing you don't render.
Characters to keep: JAD APPS Glyphs kept: 7 (J A D ␣ P S + .notdef) Output: JADApps-Bold.subset.ttf (~5 KB raw TTF) Next: TTF -> WOFF2 -> ~2 KB
Logo + tagline + year
ExampleWhen the mark includes a tagline and a copyright year, every unique glyph across both strings goes in one box. Build the set by concatenating logo and tagline text, then deduplicate by eye (the tool deduplicates by codepoint anyway).
Characters to keep:
JAD APPS — Build Faster 2026
Unique glyphs: J A D ␣ P S — B u i l d F a s t e r 2 0 6
Glyphs kept: 22 + .notdef
Note: the em dash (—, U+2014) must be the real character,
not a hyphen (-), or the dash renders as .notdefCompress the subset to WOFF2 (the missing step)
ExampleThe Whitelist Builder outputs TTF only. This is the second tool you run — TTF→WOFF2 — to reach hero-font sizes. There is no WOFF2 option inside the Whitelist Builder itself.
Step 1 Whitelist Builder -> JADApps.subset.ttf (~5 KB) Step 2 /font-tools/ttf-to-woff2 -> JADApps.subset.woff2 (~2 KB) Why: a raw TTF on the wire is 2-3x larger than its WOFF2. Never ship the .subset.ttf directly to production.
Embed the tiny logo font
ExampleInline the ~2 KB WOFF2 as a data URI for a single render-blocking-free request, or link it. Generate the block with the Font-Face Generator rather than hand-writing it.
@font-face {
font-family: "BrandMark";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(/fonts/JADApps.subset.woff2) format("woff2");
}
.logo { font-family: "BrandMark", sans-serif; }
/* Build this block: /font-tools/font-face-generator */Sanity-check coverage before you build
ExampleIf a logo glyph is missing from the source font, the Whitelist Builder silently maps it to .notdef rather than erroring (unless every glyph misses, which errors). Confirm the source has your characters first.
Before whitelisting 'CAFÉ®': Run /font-tools/character-coverage-map on the source font. Confirm it lists É (U+00C9) and ® (U+00AE). If É is absent, the rendered logo shows a box where É should be.
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.
The downloaded file is a TTF, not the tiny WOFF2 you expected
By designThe tool outputs <stem>.subset.ttf — uncompressed TrueType — every time. opentype.js's Font.toArrayBuffer() writes sfnt/TTF; there is no WOFF2 option in the UI. The well-known "1.5–3 KB logo font" figure is the size after you run the TTF through TTF→WOFF2. The raw TTF for a handful of glyphs is more like 4–8 KB. Don't ship the .subset.ttf to the web — convert it first.
Letter-pair spacing looks loose ('AV', 'To', 'WA')
Kerning droppedThe subset is built as a fresh font from new Font({ glyphs }) — it carries unitsPerEm, ascender, descender, and the kept outlines, but not the kern/GPOS tables. So pair-kerning is gone. For a caps geometric logo this is often invisible; for a wordmark with classic kern pairs (AV, To, Ya) the gaps will look default. If kerning is essential, keep the full font and use CSS, or use a layout-preserving subsetter (hb-subset/pyftsubset) in a build pipeline instead — the browser tool cannot.
A ligature in the logo renders as separate letters
OT features droppedGSUB is dropped along with the rest of the layout tables, so an fi/ffl ligature or a stylistic-set swash that the source applied via OpenType features will not fire in the subset. The base glyphs render individually. If the logo's look depends on a liga/dlig/ss01 feature, the whitelist approach will change its appearance — bake the look in at the design stage or keep the feature-bearing font.
A character you pasted shows as a blank box (.notdef)
Coverage gapIf the source font has no glyph for a codepoint you pasted, that codepoint simply isn't kept and the text renders .notdef (a box). The tool does not warn per-character — it only errors when zero of your characters match the font at all. Check coverage with the Character Coverage Map before building a mark with accents, symbols, or non-Latin glyphs.
Empty whitelist or a whitelist that matches nothing
ErrorAn empty 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" and suggests running the Character Coverage Map. Both are hard failures, not silent — you get a clear message rather than a broken file.
OTF / CFF (PostScript-outline) font fails to write
Writer erroropentype.js's font writer struggles with CFF (PostScript) outlines and some non-standard tables. If your display face is an OTF with CFF outlines, Font.toArrayBuffer() can throw — the tool surfaces "Subsetting failed in opentype.js's font writer." Convert the OTF to a TrueType-outline TTF in a desktop tool first, or subset with pyftsubset/hb-subset in a pipeline.
You forgot the space and 'JAD APPS' collapses to 'JADAPPS'
ExpectedThe space character is U+0020 — a real glyph in the whitelist. If you paste JADAPPS with no space, the space glyph is dropped, and any text using a space will render .notdef where the gap should be. Always include the space if your mark has one. Conversely, only the leading/trailing whitespace of the whole textarea is trimmed; inner spaces are kept.
Hinting is preserved but outlines may look soft at tiny sizes
PreservedOutlines are copied verbatim, and TrueType instructions attached to kept glyphs ride along in the rebuilt font. But because a logo is usually rendered large, hinting rarely matters here. If you want the smallest possible file and don't need hinting, run the subset through the Hinting Stripper before the WOFF2 step.
Brand name fields (foundry, license) carry over the source font's identity
Carried overThe subset's family name is taken from the source font's English fontFamily (or the filename stem if absent). The full original name table is not faithfully reproduced, but treat the subset as still bound by the source font's license — subsetting is not relicensing. If you need to strip name-table bloat for size, use the Name Table Cleaner afterward.
Frequently asked questions
How small can my logo font actually get?
The tool outputs an uncompressed TTF, typically 4–8 KB for a 6–15 glyph wordmark. The headline ~1.5–3 KB figure is the size after you run that TTF through TTF→WOFF2, which is a separate tool. So the real chain is: paste characters → download .subset.ttf → convert to .woff2 → embed. Don't expect WOFF2 directly out of the Whitelist Builder; it doesn't offer that format.
Will spaces and punctuation be included?
Only what you paste. The space character is U+0020 — if your wordmark has a space, include it in the box. Same for an ampersand, em dash, registered-trademark symbol, or any punctuation: each is a codepoint that must appear in 'Characters to keep' or it becomes .notdef. Leading/trailing whitespace around the whole box is trimmed, but inner spaces are kept as real glyphs. The .notdef glyph (glyph 0) is always preserved automatically, as the spec requires.
Does it keep kerning between my logo letters?
No. The subset is rebuilt as a fresh font carrying the kept outlines, units-per-em, ascender, and descender — but not the kern/GPOS tables. The result panel says so: "Kerning + OT layout features dropped; outlines preserved." For a geometric caps logo the default spacing is usually fine; for a wordmark with classic kern pairs (AV, To, WA) you'll see looser gaps. If kerning is critical, keep the full font, or use a layout-preserving subsetter (hb-subset/pyftsubset) in a build pipeline — the in-browser tool can't preserve those tables.
What input formats can I upload?
TTF, OTF, WOFF, and WOFF2. WOFF is inflated (zlib) and WOFF2 is decompressed (Brotli) to a raw sfnt before subsetting, so there's no need to convert beforehand. One caveat: OTF fonts with CFF (PostScript) outlines can trip opentype.js's writer on output — if that happens you'll get a clear 'writer failed' error, and the fix is to convert to a TrueType-outline TTF first.
Does it handle multi-codepoint characters like emoji ZWJ sequences?
It handles each codepoint individually, not as a sequence. The whitelist is built by decoding your string into individual codepoints (codePointAt) and keeping glyphs whose Unicode value matches. A precomposed character like é (U+00E9) works as one glyph. But a ZWJ emoji sequence or a base + combining-mark cluster is split into its component codepoints — paste each part, and the result depends on the font having those base glyphs. For a logo this rarely comes up; logos are almost always plain Latin caps.
Why does my logo show a box where a character should be?
That's .notdef — it means the source font has no glyph for a codepoint you pasted (or you pasted a character that isn't actually in your mark). The tool only errors when none of your characters match the font; partial misses are silent. Run the Character Coverage Map on the source font first to confirm it contains every glyph in your mark, especially accents, symbols, and the em dash.
Is my brand font uploaded anywhere?
No. Parsing and subsetting run entirely in your browser via opentype.js. An unreleased or licensed display face never reaches a server. The only thing recorded server-side is a usage counter for signed-in dashboard stats (no font content). This matters for logo work, where the display face is often a single-purpose licensed font you're not allowed to redistribute.
Do I need a paid plan to use this?
Yes — it's a Pro-tier tool (minTier: pro). The Pro font-file limit is 50 MB, which is far above any logo font. Free tier can't run it. If you only need a one-language subset rather than an exact character list, the Latin Filter and Font Subsetter cover charset-based subsetting.
What's the difference between this and the Font Subsetter?
The Font Subsetter keeps a Unicode range (Latin, Latin-Ext, Cyrillic, Greek, Vietnamese, symbols) — good for body text in a known language. The Whitelist Builder keeps an exact character list — good for a fixed string like a logo or headline where you know every glyph in advance. Both share the same in-browser subset engine and both output TTF; they differ only in how you specify what to keep. See whitelist vs charset strategies.
Can I include the same letter twice to make it 'heavier'?
No — pasting a duplicate codepoint has no effect. The whitelist is a Set of codepoints, so 'APPS' contributes one P, not two. Glyph weight comes from the source font's design; if you want a bolder mark, start from the bold weight of the family before subsetting.
Will the subset still display in design tools like Figma?
Yes — it's a valid TTF and installs/imports like any font. But because it contains only your whitelisted glyphs, typing any other character in Figma shows .notdef boxes. That's expected for a logo font: it's a single-purpose file, not a general typeface. Keep the full font installed for general design work and use the subset only for the production wordmark.
How do I keep the output as small as possible?
Three levers: (1) whitelist the absolute minimum set of glyphs — drop lowercase if your mark is all caps; (2) run the Hinting Stripper on the subset if you don't need TrueType hinting (logos render large, so you usually don't); (3) convert to WOFF2 last with TTF→WOFF2. The biggest win is always the WOFF2 step — it's where the 2–3× compression happens.
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.