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 400wght 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 (Python fonttools) and glyphhanger handle 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 wght axis to 400 600 so 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-serif from typography/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-face for you. Wiring @font-face rules, choosing font-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.