How to system font stack vs custom web font: when to pick which
- Step 1Decide what the surface optimises for — Body and UI text optimise for speed and legibility — readers won't clock San Francisco vs Segoe UI. Headlines, logos, and marketing pages optimise for brand. Split your surfaces along that line before choosing a font strategy.
- Step 2Build the system stack for the speed surfaces — In this generator, pick a Style (Sans-serif for UI) and OS scope (All for a public site), then **Generate**. You now have a zero-byte body font with no loading strategy to manage.
- Step 3Reserve a custom web font for brand surfaces only — If a surface needs brand consistency, declare exactly the weights you need with the [Font Face Generator](/font-tools/font-face-generator). One or two weights for headlines is far cheaper than a full family for body.
- Step 4Pick a loading strategy for the custom font — A custom font needs a `font-display` value — use the [font-display Strategy Picker](/font-tools/font-display-strategy). The system stack needs none, because it downloads nothing.
- Step 5Preload only the critical custom font — If the brand headline is above the fold, preload that one file with the [Preload Tag Builder](/font-tools/preload-tag-builder). Never preload system fonts — there's nothing to fetch.
- Step 6Measure before and after — Compare LCP and total transferred bytes with and without the custom font. If the brand gain doesn't justify the byte and layout-shift cost on body text, keep body on the system stack and stop there.
System stack vs custom web font
The core trade-offs. The system stack is what this generator produces; the custom-font column is what you take on when you add @font-face.
| Dimension | System font stack | Custom web font |
|---|---|---|
| Bytes downloaded | 0 KB | ~50-200 KB across weights |
| First paint | Instant — OS font already present | Depends on font-display: block (FOIT) or swap (FOUT) |
| Layout shift (CLS) | None from the font | Possible reflow when the web font swaps in |
| Brand consistency | Varies per OS (SF / Segoe UI / Roboto) | Identical everywhere |
| Privacy / GDPR | No external request | Self-host to avoid third-party CDN requests |
| Loading strategy needed | None | font-display + optional preload |
| Maintenance | Static list, never changes | Subset, version, cache-bust on update |
The hybrid most sites land on
How real teams split the two approaches by surface. This generator covers the system rows; the sibling tools cover the custom row.
| Surface | Strategy | Tool |
|---|---|---|
| Body / UI text | System stack (sans-serif, All) | This generator |
| Code blocks | System monospace stack | This generator (Style = Monospace) |
| Long-form prose | System serif stack OR one web serif | This generator, or Font Face Generator |
| Headlines / hero | One brand web font weight | Font Face Generator + font-display |
| Above-the-fold brand text | Preloaded brand font | Preload Tag Builder |
When to pick which
A quick decision matrix. 'Both' means the hybrid.
| If your priority is… | Pick | Why |
|---|---|---|
| Lowest LCP / fastest paint | System stack | Zero font bytes, no swap window |
| One brand identity everywhere | Custom web font | Same glyphs on every OS |
| Privacy with no CDN calls | System stack (or self-hosted font) | No third-party request |
| Distinctive marketing pages, fast app | Hybrid | Brand font for hero, system for app body |
| A design system that must scale teams | System body default + opt-in brand | See the policy guide |
Cookbook
Concrete before/after comparisons showing the byte and behaviour difference, and how the hybrid is wired in CSS.
Pure system stack — zero font bytes
ExampleThe performance-first choice. No @font-face, no preload, no font-display. First paint shows real text immediately because the OS font is already loaded.
/* Generated here: sans-serif, all */
:root { --font-system-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; }
body { font-family: var(--font-system-sans); }
Font bytes transferred: 0
FOUT/FOIT window: none
CLS from font: 0Pure custom web font — one identity, real cost
ExampleThe brand-first choice. You declare @font-face, pick a font-display value, and accept a download plus a swap window. Shown for contrast — these are NOT produced by this generator.
/* From Font Face Generator + font-display picker (not this tool) */
@font-face {
font-family: "Brand Sans";
src: url(/fonts/brand-sans.woff2) format("woff2");
font-weight: 400;
font-display: swap;
}
body { font-family: "Brand Sans", sans-serif; }
Font bytes transferred: ~80 KB (one weight)
FOUT window: until brand-sans.woff2 arrivesHybrid — system body, brand headline
ExampleThe pattern most production sites use. Body uses the zero-byte system stack; only H1/H2 pull the brand font. The brand font is small (one or two weights) and degrades to the system stack while loading.
/* System half — this generator */
:root { --font-system-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; }
body { font-family: var(--font-system-sans); }
/* Brand half — Font Face Generator, headlines only */
h1, h2 { font-family: "Brand Display", var(--font-system-sans); }
Body bytes: 0 · Headline bytes: ~30 KB (one display weight)System monospace vs a code web font
ExampleCode blocks rarely justify a downloaded font. The system monospace stack gives every reader a sensible coding font with zero bytes; a web mono font adds weight for a marginal aesthetic gain.
/* System: this generator, Style = Monospace */
pre, code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
/* vs. a web mono font: +40-90 KB for JetBrains Mono / Fira Code
— only worth it if ligatures/branding are a real requirement */Privacy-driven: system stack to avoid CDN calls
ExampleIf you must avoid third-party requests (GDPR, no-cookie marketing), the system stack makes zero external calls by definition. The alternative is self-hosting the web font — more work but keeps the brand identity.
/* Zero external requests — nothing leaves the browser */
body { font-family: var(--font-system-sans); }
/* If brand is non-negotiable: self-host instead of Google Fonts CDN
→ use Google Fonts CSS Generator / Font Face Generator to bundle
the font into your own /fonts/ directory */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.
Expecting the generator to also build the web font
By designThis tool only produces the system half — a CSS variable plus a body rule. It has no @font-face output and no upload. For the custom-font side of a hybrid, use the Font Face Generator; for self-hosting a Google font, use the Google Fonts CSS Generator.
Web font and system stack render at different metrics
Layout shiftWhen a web font swaps in over the system fallback, glyph widths and line-height often differ, causing a reflow (CLS). A system stack never shifts because nothing swaps. If you go hybrid, mitigate the swap with size-adjust/ascent-override on the @font-face and a matched fallback — out of scope for this tool, but worth knowing before you add a web font.
Brand consistency lost across OSes with system stack
Trade-offA system stack looks like San Francisco on Apple, Segoe UI on Windows, Roboto on Android — three slightly different typefaces. For body copy this is invisible to readers; for a wordmark or a tightly art-directed hero it is unacceptable. That's the exact line where you switch that surface to a custom font.
Custom font hurts LCP if it's render-blocking
Performance riskWith font-display: block (the browser default in some cases), text is invisible until the web font arrives — directly inflating LCP on slow networks. A system stack has no such window. If you add a web font, choose swap or fallback explicitly via the font-display Strategy Picker.
Self-hosting needed for privacy even with a custom font
ComplianceLoading a font from a third-party CDN can leak the visitor's IP (the basis of past Google Fonts GDPR rulings in the EU). A system stack avoids this entirely. If brand requires a custom font, self-host it — bundle it into your own origin with the Google Fonts CSS Generator rather than hot-linking the CDN.
System serif differs more across OSes than system sans
Trade-offThe sans-serif system fonts (SF, Segoe UI, Roboto) are all humanist sans and look fairly similar. The serif defaults vary much more — Iowan Old Style vs Cambria vs Roboto Slab. So for long-form prose where consistency matters, a single web serif is a more defensible spend than for body sans.
Hybrid still needs the fallback in the brand selector
RequiredWhen you set h1 { font-family: "Brand Display", … }, always include the system stack variable after the brand name. Otherwise, while the brand font loads (or if it fails), the heading falls to the browser default rather than your tuned system stack. The hybrid recipe in the cookbook shows the correct form.
One web font weight is cheap; a full family is not
Cost scalingThe byte cost scales with weights and styles. One display weight (~30 KB) for headlines is a small, cacheable spend. A four-weight family with italics for body text is 150 KB+ and a much bigger LCP/CLS exposure. The hybrid keeps the expensive surface (body) on the free system stack.
Frequently asked questions
Is a system stack always faster than a web font?
For loading, yes — it downloads zero font bytes and paints real text on the first frame, with no FOUT/FOIT and no font-driven layout shift. A web font always costs at least one request and introduces a swap or block window. The web font's advantage is brand consistency, not speed.
How many bytes does a custom web font cost?
Roughly 50-200 KB depending on how many weights and styles you load and how aggressively you subset. One display weight can be ~30 KB; a full body family with italics is often 150 KB+. A system stack is 0 KB regardless.
What's the hybrid approach and why is it popular?
System stack for body/UI (where per-OS differences are invisible to readers) plus one brand web font for headlines and hero surfaces. It keeps the expensive surface free and fast while still expressing brand where it counts. Most production sites converge on this.
Does this generator build the custom web font part too?
No. It only produces the system stack — a CSS variable and a body rule. For @font-face, use the Font Face Generator; for self-hosting a Google font, the Google Fonts CSS Generator; for the loading value, the font-display Strategy Picker.
Will my brand look inconsistent if I use a system stack?
Body text will render in each OS's native font (San Francisco / Segoe UI / Roboto), which readers don't consciously notice. Wordmarks, logos, and art-directed headlines do look different — that's why the hybrid moves only those surfaces to a custom font.
Does a system stack avoid GDPR font issues?
Yes — it makes no external request, so there's no visitor IP sent to a font CDN. If you need a custom font but still care about privacy, self-host it on your own origin rather than hot-linking a third-party CDN.
What about CLS — does the font choice affect it?
A system stack contributes no font-driven CLS because nothing swaps in. A web font can reflow text when it swaps over the fallback, unless you match metrics with size-adjust/ascent-override. That's another reason to keep body text on the system stack in a hybrid.
Should code blocks use a web font or the system mono stack?
Usually the system monospace stack — it gives every reader SF Mono / Consolas / JetBrains Mono with zero bytes. A web mono font (e.g. for ligatures) only pays off if those features are a genuine requirement, not just aesthetics.
If I add a web font, do I still need the system stack?
Yes — as the fallback. Put the system stack variable after the brand name in every brand selector (font-family: "Brand", var(--font-system-sans)). It's what renders while the web font loads and if it fails, so the page never falls to an untuned browser default.
Which is better for accessibility?
Both can be accessible; the bigger accessibility lever is using rem sizes that respect the user's browser font-size preference. A system stack has a slight edge in that it never leaves text invisible during a load window (FOIT), which matters on slow connections.
How do I decide per surface instead of per site?
Map each surface to its priority: speed → system stack; brand → custom font. Body, UI, and code lean system; hero, logo, and marketing lean custom. The decision matrix table above turns this into a quick lookup. The design-system policy guide formalises it for a team.
Can I prototype the hybrid quickly?
Yes — generate the system stack here in two clicks, wire it to body, then add a single brand weight on h1, h2 via the Font Face Generator. Measure LCP and bytes before and after to confirm the brand gain is worth the cost.
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.