system-ui vs ui-sans-serif Generic Families
This guide is part of the Native System Font Stacks topic, within the broader Typography Fundamentals & System Architecture area.
Two CSS mechanisms claim to give you "the OS's native UI font" for zero bytes over the wire: the system-ui font-family keyword, shipped since 2017, and the newer ui-sans-serif / ui-serif / ui-monospace / ui-rounded generic family values added in CSS Fonts Module Level 4. They sound interchangeable and most engineers pick whichever one they saw in a blog post first. They are not interchangeable. system-ui is a single opaque keyword that resolves to whatever the platform calls its default UI typeface, with no guarantee of which specific typeface that is, while the ui-* generics are a small family of purpose-scoped tokens that let you ask for a serif, monospace, or rounded system font specifically instead of only ever getting the default sans. Picking the wrong one produces a stack that silently falls through to a Chrome-shipped fallback on the very platforms you thought you had covered, and depending on how you resolved the font-fallback-for-cjk-and-emoji problem elsewhere in your stack, that fallthrough can reintroduce the exact CLS and FOUT problems a system font stack is supposed to eliminate.
Prerequisites
You should already understand what a system font stack buys you: zero network request, zero parse-blocking @font-face resolution, and instant text paint because the glyphs are already rasterized on the user's device. If you have not built one yet, start with Building a Cross-Platform system-ui Stack, which covers the historical -apple-system / BlinkMacSystemFont / Segoe UI fallback chain that system-ui was introduced to replace. You also need a basic grasp of CSS generic font families — serif, sans-serif, monospace — because the ui-* values behave exactly like those generics: they are resolved by the user agent, not looked up in an installed font list, and they always sit last in a font-family list as the guaranteed-to-match term.
You do not need any build tooling, subsetting pipeline, or @font-face block for this page — that is the entire point of a system font stack. If your project also renders CJK or emoji glyphs, keep in mind that neither system-ui nor ui-sans-serif guarantees coverage for those scripts; you still need the segmented fallback approach described for CJK and emoji separately.
Implementation
system-ui is a single keyword. It has one job: resolve to the platform's default UI font, whatever that happens to be. It carries no information about weight ranges, script coverage, or whether the resolved font is a sans, serif, or something else — on every platform shipped since 2017 it has resolved to a sans, but that is convention, not a guarantee written into the spec.
:root {
--font-ui: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Helvetica, Arial, sans-serif;
}
body {
font-family: var(--font-ui);
}
Here, system-ui is tried first. On macOS and iOS Safari it resolves to San Francisco. On Windows it resolves to Segoe UI (or Segoe UI Variable on Windows 11 builds that support it). On Android it resolves to Roboto, or Noto Sans on some OEM skins. On Linux desktops running GNOME or KDE it resolves to whatever the desktop environment's UI font is set to, commonly Cantarell, Noto Sans, or Ubuntu. The legacy -apple-system and BlinkMacSystemFont entries after it are dead weight on any browser new enough to support system-ui — every engine that recognizes -apple-system also recognizes system-ui — but they cost nothing to keep as a defensive no-op for ancient WebKit builds, and removing them buys you no measurable byte savings since they are keywords, not files.
The ui-* generics resolve differently: instead of one keyword covering "the UI font, whatever style it is," you get four scoped tokens.
:root {
--font-ui-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-ui-serif: ui-serif, Georgia, "Times New Roman", serif;
--font-ui-mono: ui-monospace, "SF Mono", "Cascadia Code", "Consolas", monospace;
}
.prose { font-family: var(--font-ui-serif); }
code, pre { font-family: var(--font-ui-mono); }
ui-sans-serif asks the platform specifically for its default sans UI face — on most platforms this resolves to the same typeface system-ui would give you, so for a plain UI sans stack the two are functionally redundant and system-ui alone is simpler. The real value of the ui-* family shows up with ui-serif and ui-monospace: there is no serif or monospace equivalent of system-ui at all. If you want the platform's native monospace face — SF Mono on Apple platforms, Cascadia Code or Consolas on Windows, a DejaVu or Noto Mono derivative on Linux — ui-monospace is the only keyword-based way to ask for it without hardcoding a platform-specific font-family list yourself. ui-rounded similarly requests a rounded-terminal variant where the platform ships one, such as SF Pro Rounded on Apple devices; on platforms with no rounded system face it falls through to the next stack entry.
A defensive cross-browser variant
Support for the ui-* generics lags system-ui. Safari has supported all four since Safari 11 (2017), matching its system-ui support. Chrome and Edge added them in Chromium 106 (October 2022) — meaning any Chrome release from 2017 through 2022 understood system-ui but not ui-monospace or ui-serif and would fall through to the next stack entry. Firefox shipped ui-* support in Firefox 92 (2021). Because a font-family list is resolved left to right with the browser simply skipping any keyword it does not recognize, you never need feature detection or @supports — you only need the fallback entries to be genuinely equivalent so older engines land somewhere reasonable instead of an unstyled default.
code, kbd, samp, pre {
font-family:
ui-monospace,
"SF Mono",
"Cascadia Code",
"Roboto Mono",
Menlo,
Consolas,
"Liberation Mono",
monospace;
}
The explicit named fonts here are not decorative padding — they are the actual defensive layer. A Chrome 90 user on Windows never resolves ui-monospace (unsupported keyword, silently skipped), falls through past SF Mono and Cascadia Code (not installed unless the user has VS Code with Cascadia Code system-installed), and lands on Consolas, which ships with every Windows install since Vista. A Firefox 80 user on Linux skips ui-monospace, skips the three named proprietary fonts, and lands on the generic monospace, which on Linux resolves to whatever the distro's monospace font-config default is — usually DejaVu Sans Mono. Test this fallthrough explicitly rather than assuming it: open DevTools on an older engine (or use a browser's user-agent override plus the Rendering panel) and confirm the computed font in the Elements panel's Computed tab actually matches an installed, monospaced face — a proportional fallback landing in a code block is a silent, easy-to-miss regression that only shows up as slightly-off character alignment.
Verification
Open Chrome DevTools, select a heading or body element styled with your --font-ui custom property, and check the Computed tab's font-family row — it lists the full cascade you wrote, but the Rendered Fonts section (in the Layout or Computed panel, depending on Chrome version) shows which single font actually got used. On macOS this should read .SFNS-Regular or -apple-system internally, on Windows it typically reports Segoe UI. Repeat this check with the OS's own accessibility "increase font size" or "bold text" setting toggled on — genuine system font stacks respect those settings automatically because the resolved font is the OS's live UI font object, not a static asset; a network-loaded web font never picks up that OS-level preference.
For the ui-monospace case, run the same Computed-tab check against a <code> element and confirm the rendered family is a true monospace face (equal advance width per glyph) rather than a proportional fallback that merely looks code-like at a glance. You can also script a coarse check with document.fonts.check(), though note it only tells you whether a named face string would match an already-loaded font — it cannot resolve generic keywords like system-ui or ui-monospace, so for those you must rely on the Computed panel or a visual diff, not a JS API call.
Common Pitfalls
- Assuming
ui-sans-serifgives broader coverage thansystem-ui. For plain body/UI text they resolve to the same platform font on every major browser; the extra keyword adds no coverage and one more no-op parse step. Usesystem-uialone unless you are also usingui-seriforui-monospaceelsewhere and want a consistent naming convention across the stack. - Dropping the named fallback fonts because "the OS always has one." Older engines that do not recognize
ui-monospaceorui-serifskip the keyword entirely and fall to whatever is next — if that is a bare generic likemonospace, you get an inconsistent face per OS/distro instead of a deliberately chosen one. Always keep 2–3 named fonts between the generic keyword and the terminal fallback. - Using
system-uiwhere you actually need a specific brand typeface reliably.system-uiresolves differently per platform by design — it is not a way to guarantee a single visual typeface across devices. If your brand requires one consistent face everywhere, you need a self-hosted web font, not a system stack; conflating the two goals produces inconsistent screenshots across a design review. - Forgetting that
system-uihad a real Android/Chrome bug. Chrome on Android briefly (Chrome 56–61-era builds) resolvedsystem-uitoRobotoeven on devices where the OEM had swapped the system font, ignoring the user's chosen device font. If you support very old Android WebViews, verify actual rendered output on-device rather than trusting the keyword blindly, and keep an explicitRobotofallback entry regardless since it is the correct behavior on stock Android anyway. - Applying
ui-serifto body copy expecting typographic quality control. The platform serif resolved byui-serif(Georgia-like on many systems, but not guaranteed) was designed for OS chrome, not long-form reading; if editorial line length and color matter to your design system, evaluate the actual resolved face per platform before committing, per the guidance in Fallback Font Stack Design. - Skipping metric verification when a system font stack sits next to a webfont-loaded heading. If your headings use a self-hosted display face and body text uses
system-ui, an unmatched x-height or cap-height between the two can produce a visible size mismatch even though neither element individually shifts layout; check both against the guidance in Font Metrics & Baseline Alignment before shipping.
Frequently Asked Questions
Does system-ui ever trigger a network request like a web font does?
No. system-ui and all four ui-* values are keywords resolved entirely by the browser/OS font-matching layer against fonts already present on the device — there is no @font-face src involved and no HTTP request, so they contribute 0 bytes to the page's transfer size and 0ms to font-related render-blocking time, unlike any @font-face-declared family.
Can I use ui-monospace as the only entry with no fallback, since "every modern browser supports it"?
You can, but you shouldn't. Chromium-based browsers only gained ui-monospace support in version 106 (October 2022); any Chromium build older than that — including some enterprise-locked or embedded WebView deployments still in the field — silently skips the keyword. If it is the last entry in your list with nothing after it, those browsers fall back to the browser's own default font, which is rarely monospaced. Always keep at least one concrete named fallback plus the bare monospace generic after it.
Is system-ui deprecated now that the ui-* generics exist?
No. system-ui remains the correct, simpler choice for a general UI sans stack and is not being phased out — it has broader legacy browser support than the ui-* family and does exactly one well-defined job. The ui-* generics are additive, covering the serif/monospace/rounded cases system-ui was never designed to address, not a replacement for it.
Do these keywords respect a user's OS-level "bold text" or dynamic type accessibility setting?
Generally yes, because the browser resolves them to the live system font object rather than a static file, and OS accessibility settings that affect system UI rendering (bold text on iOS, larger system font on some Android skins) apply at that same resolution layer. A self-hosted @font-face file has no such hook and will not respond to those settings without separate prefers-contrast or user-stylesheet handling.