Recommendation: variable typography
Why we recommend a variable font for $family-sans-serif,
what bundle, paint, and motion gains you get, and what you trade off
in exchange. The framework itself never ships a typeface — this page
is about the format choice consumers make on their side.
Recommendation. When you wire a typeface into Krysalicss via
typography/base.$family-sans-serif, prefer a variable font (e.g. Inter, Public Sans, Roboto Flex, Recursive) over a stack of static cuts.
The framework doesn't ship a font and never will: typography/base is
deliberately family-agnostic so consumers can match their brand. But
the choice of typeface format has a measurable impact on bundle size,
paint stability, and the kind of motion your interactive states can
afford. This page is the rationale; the wiring lives in the
Typography reference.
Why a variable font
Bytes: one file replaces three to five
A variable font ships a single .woff2 whose weight axis (wght)
covers 100–900 in one continuous range. The browser interpolates any
intermediate weight for free. A typical product UI loads four static
cuts (Regular, Medium, SemiBold, Bold) plus their italics: eight
files. A subsetted variable cut covering the same axis range is
usually 30–50 % smaller than the smallest two static cuts combined,
let alone the full eight.
Smaller transfer means a faster first paint, and the saving compounds on cold caches (mobile, low-bandwidth, repeat-visit churn).
Requests: one HTTP/2 stream, no race conditions
Eight font files means eight in-flight requests, each subject to its
own connection-pool slot, its own Content-Encoding, and its own
flake risk on a marginal connection. Modern browsers serialise font
loads in tiers: critical text waits on Regular, fallback text on
Medium, decorative emphasis on Bold. A variable font collapses
those tiers into a single stream: once the file lands, every
weight in the document paints at once, no FOUT cascade, no
progressive bolding.
Motion: smooth interpolation, no crossfade
font-variation-settings: 'wght' 600 interpolates against any other
wght value as a numeric CSS property. That means a :hover
transition from wght 400 → wght 600 slides through 401, 402, …
599: no perceptible flicker, no double-paint as one static cut
unloads and another loads. Static cuts can't animate weight at all
without crossfade hacks.
This matters mostly for component-level affordance: button label
weight on hover, focus emphasis on form labels, tab-active emphasis
in dense navigation. The framework doesn't ship those animations
itself (it's JS-free CSS), but the JS-free :hover / :focus-visible
selectors can drive them on a variable font without any extra
markup.
Accessibility: fine-grained weight for low-vision readers
WCAG 1.4.4 ("Resize text") requires UIs to remain usable at 200 %
zoom. Some low-vision readers prefer slightly heavier body weight
without crossing into semibold visual jaggedness: a font-weight: 450 that variable fonts deliver natively, but static cuts can't.
Authoring a prefers-contrast: more block that bumps body weight
by 50 units is one variable-font CSS rule; on static cuts it
requires shipping an extra weight file.
The framework doesn't enforce a prefers-contrast body-weight bump
out of the box, but the door stays open for consumers who want it
once they're on a variable font.
Tradeoffs
- One file is bigger than one static cut. A subsetted Inter Variable .woff2 lands around 130 KB; an Inter Regular static .woff2 is around 90 KB. The break-even is two cuts: as soon as you'd ship Regular + Bold, the variable file is the cheaper bundle.
- Subsetting still applies. Tools like
pyftsubset(Pythonfonttools) andglyphhangerhandle variable fonts the same way they handle static ones: drop unused glyph ranges, keep the axes you need, drop the rest. A Latin-only Inter Variable subset is well under 60 KB. - Older browsers don't matter for our baseline. Variable-font
rendering (
font-variation-settings) is Baseline since 2020: the same evergreen-last-2-years window Krysalicss already targets (see Design philosophy). - Pick the right axis range at subset time. If your design only
uses Regular and SemiBold, subset the
wghtaxis to400 600so the file doesn't carry weight ranges you'll never paint. Most variable subsetters expose this knob.
What we don't do
- We don't ship a font. The framework reads
$family-sans-seriffromtypography/base, defaulting to a system stack. - We don't enforce a variable-font requirement. A static-cut consumer works exactly as before: the recommendation is for new projects picking a typeface today.
- We don't
@font-facefor you. Wiring@font-facerules, choosingfont-display, and self-hosting vs. CDN are consumer decisions outside the framework's scope.
Pointer
For the actual @use invocation that swaps the family in, plus the
full typography/base variable surface, see the
Typography reference. The
override snippet there shows where $family-sans-serif goes.
For broader font-loading discipline (preload, font-display,
self-hosting), Fontsource and
modern font stacks are the canonical
external references: both are framework-agnostic and align with
what we recommend here.