@gamesome/core-font
v0.1.0
Published
Core font optimization logic for generating @font-face CSS with fallback scaling
Readme
@gamesome/core-font
Core font optimization logic shared by @gamesome/astro-font and @gamesome/style-dictionary-font. This package generates @font-face CSS with scaled fallback fonts to reduce Cumulative Layout Shift (CLS).
You probably don't need to use this package directly — use one of the framework-specific packages above instead. This README documents the approach, algorithm, and known limitations.
How fallback scaling works
When a web font loads, the browser swaps it in for the fallback font. Because web fonts and system fonts have different metrics (character widths, ascenders, descenders), this swap causes text to reflow. The goal is to make the fallback font match the web font's dimensions as closely as possible, so the swap is invisible.
We use Capsize to calculate CSS override values for each fallback font. This is the same approach used by @next/font and fontaine. The key CSS descriptors are:
size-adjust— scales all glyph metrics by a percentage, making the fallback font wider or narrower to match the web font's average character widthascent-override— adjusts the space above the baselinedescent-override— adjusts the space below the baselineline-gap-override— adjusts the extra space between lines
The algorithm
For regular weight, we delegate to Capsize's createFontStack() which computes override values based on per-font metrics from @capsizecss/metrics. The core calculation is:
- Compute the average character width ratio:
(webFont.xWidthAvg / webFont.unitsPerEm) / (fallback.xWidthAvg / fallback.unitsPerEm)— this becomessize-adjust - Recompute vertical metrics against the size-adjusted em square — these become
ascent-override,descent-override, andline-gap-override - Only emit overrides that differ from the fallback font's native values
Bold fallback calculation
Bold text is wider than regular text, and different fonts grow at different rates when bolded. Without weight-specific overrides, a fallback @font-face that looks good at regular weight can be significantly off at bold.
Capsize's createFontStack() only generates a single set of overrides (for regular weight) and doesn't export its internal calculateOverrideValues function. We replicate that calculation using weight-specific variant metrics from Capsize's metrics collection when that exact weight exists (for example variants["700"] for bold), which include xWidthAvg, ascent, descent, lineGap, and unitsPerEm for that cut of each font.
The bold fallback @font-face block uses the same font-family name as the regular fallback and adds the configured font-weight, so the browser selects the right override set automatically.
Why bold fallbacks matter
Browsers don't reliably synthesize bold for fallback fonts (source). Without a bold @font-face declaration using local('Helvetica Bold') or similar, bold text in the fallback font may render at regular weight — causing a large reflow when the web font loads and the text suddenly becomes wider.
User overrides
If the automatic bold calculation doesn't produce good results for a specific font pairing, you can override individual values via the bold.scaling property on fallback fonts. These values take precedence over the calculated ones. See the FallbackFont configuration in your framework package's README.
If you set a custom bold.weight, automatic calculation only works when Capsize exposes metrics for that exact weight. Otherwise we throw a build error that tells you to use bold.scaling, change the weight to 700, or disable the bold fallback.
Limitations
size-adjust scales uniformly
The CSS size-adjust descriptor scales both width and height by the same factor. There is no CSS mechanism to scale them independently. This was explicitly discussed in CSSWG Issue #6075 — independent horizontal/vertical scaling was deferred as a future enhancement and has not been revisited.
In practice, this means size-adjust optimizes for horizontal line-length matching (via xWidthAvg), and then the vertical override descriptors (ascent-override, descent-override, line-gap-override) correct the vertical metrics back to their proper values. This two-step approach — scale for width, then fix height — works well in most cases.
Mismatched bold growth rates
Some font pairings have very different bold growth rates. For example:
- Lora Bold is only ~3% wider than Lora Regular
- Georgia Bold is ~16% wider than Georgia Regular
When the web font barely grows when bolded but the system fallback grows significantly (or vice versa), the bold size-adjust will differ substantially from the regular one. Because size-adjust affects both dimensions, scaling Georgia Bold down to match Lora Bold's width also shrinks its height — causing visible vertical misalignment.
This is a fundamental limitation of the current CSS specification, not a bug in the calculation. The same limitation applies to every tool that uses size-adjust for fallback matching, including Capsize, Next.js, and Fontaine.
What you can do:
- Accept it for most pairings. Sans-serif fonts (Rubik/Helvetica, DM Sans/Arial, etc.) tend to have similar bold growth rates and produce good results automatically.
- Use
bold.scalingto fine-tune. For problematic serif pairings, manually set override values that balance width and height to your preference. - Disable bold fallback for specific fonts. Set
bold: falseon a fallback font to skip the bold@font-facedeclaration entirely. The browser will synthesize bold from the regular fallback (which may or may not look acceptable).
Variable fonts and bold metrics
For variable fonts (e.g. "Lora Variable"), Capsize's variants["700"] metrics come from the static bold cut of the same family (e.g. the separate "Lora Bold" font file). These metrics are a close approximation of the variable font at wght=700 — they share the same design masters — but not an exact match. This is the best data available, as variable font metrics at arbitrary weight values aren't exposed to CSS tooling.
Comparison with other tools
| Feature | @gamesome/core-font | Capsize | Next.js (next/font) | Fontaine |
| ---------------------------- | -------------------- | ----------------------- | ---------------------------------- | ------------------------------------------------------------- |
| Regular weight fallbacks | Yes (via Capsize) | Yes | Yes | Yes |
| Per-weight bold fallbacks | Yes | No (single weight only) | No | Yes (since #264) |
| Manual override escape hatch | Yes (bold.scaling) | N/A | No | No |
| Multiple fallback fonts | Yes | Yes | No (Arial or Times New Roman only) | No |
Tools
- Perfect-ish Font Fallback — visually compare a web font against system fallbacks and preview override values. Useful for choosing which fallback font pairs best with your web font.
- Font overlay bookmarklet — overlay a different font on your live page to compare
- Font style matcher — visually compare two fonts side by side
Further reading
- Chrome: Improved font fallbacks — explains the problem and the
@font-faceoverride approach - web.dev: CSS size-adjust — overview of the
size-adjustdescriptor - MDN: @font-face/size-adjust — specification reference
- Capsize — the metrics library powering the calculations
- CSSWG Issue #6075 — the proposal that became
size-adjust, including discussion of independent axis scaling
License
MIT
