Get started

CSS size-adjust for font fallbacks: match metrics without confusing font-size-adjust

Use font-size-adjust when a text element needs to preserve a chosen font metric across its font stack. Use the size-adjust descriptor to scale one defined @font-face, usually a local fallback, so its glyphs and metrics better match a primary face. The scopes differ. Even valid CSS doesn't prove that wrapping or layout movement improved.

Updated October 26, 2026

The two mechanisms are related but not interchangeable

The similar names obscure an important boundary. A declaration such as font-size-adjust: 0.52 belongs on an element and takes effect as the browser selects faces from that element's font stack. A declaration such as size-adjust: 102% belongs inside @font-face and changes the defined face itself.

`font-size-adjust` property`@font-face size-adjust` descriptor
Where it is declaredOn an element or inherited text styleInside one @font-face rule
What it affectsThe used size of selected fonts in that text's stackGlyph outlines and metrics for the defined face
Primary inputA metric keyword and value, or a browser-derived value such as from-font where appropriateA percentage multiplier derived for a specific source and target face
Best fitPreserving x-height, cap height, or another supported metric as fallback selection changesMaking a named fallback face closer to a primary face before it enters the stack
Related controlsfont-family, font-size, the selected metric, and the faces available to the browserascent-override, descent-override, line-gap-override, weight, style, and src
What it cannot proveThat the preferred file loaded, text wraps identically, or layout stayed stillThat the chosen fallback exists, every requested face maps correctly, or the final swap avoids movement
Choose the scope before choosing values.

The property can target more than lowercase x-height. MDN documents metric keywords including ex-height, cap-height, ch-width, and ic-width. That flexibility matters because a legibility problem isn't necessarily a wrapping problem. Preserving x-height may make fallback body text easier to read while leaving the line breaks different.

Do not assume the effects add together

MDN notes that font-size-adjust calculations use overridden font metrics. If a face has already been changed with the size-adjust descriptor, the property calculation can compensate for those adjusted metrics and neutralize the descriptor's apparent scaling effect. Combining both mechanisms creates a new behavior to test, not two percentages to add.

Choose from the failure you can observe

Start with the rendered failure, not the CSS feature name. Your route depends on the problem: readability, horizontal measure, vertical metrics, loading behavior, or the fallback face itself.

  1. 1

    Fallback text is too small or hard to read

    Consider font-size-adjust if preserving a supported metric across the stack addresses the problem. Test both the preferred and fallback states because the property affects font sizing, not only one temporary face.

  2. 2

    Fallback text wraps differently

    First choose a fallback with reasonably similar width characteristics. Then evaluate an adjusted fallback face. Measure width and wrapping directly because matching x-height alone doesn't guarantee matching line breaks.

  3. 3

    Text blocks have different vertical geometry

    Inspect ascent, descent, and line gap. An adjusted @font-face can pair size-adjust with the three metric-override descriptors, but every percentage must come from the actual faces being compared.

  4. 4

    The page moves when the web font arrives

    Compare the forced fallback with the loaded face, then inspect loading policy and resource timing separately. Metric overrides can reduce a geometric difference, but they don't decide whether or when a swap happens.

  5. 5

    The fallback looks wrong even when its box is close

    Choose another fallback. Scaling can't give one typeface another typeface's shapes, script coverage, texture, or character distinctions.

Metric matching is not visual cloning

Two faces can produce similar text boxes yet differ noticeably in stroke, spacing, punctuation, numerals, or script coverage. Decide whether your acceptance criterion is legibility, stable geometry, visual similarity, or a combination.

Recipe 1: preserve a metric across the stack

Use the property route when the requirement belongs to the text role, not one custom fallback face. This example is deliberately incomplete: 0.52 is a placeholder that you must replace with a value obtained for the real primary face. It isn't a recommendation for Plus Jakarta Sans or any other family.

.product-body {
  font-family: "Primary Sans", "System Fallback", sans-serif;
  font-size: 1rem;
  font-size-adjust: ex-height 0.52; /* illustrative only */
}
Illustrative property syntax. Measure the target metric for the real font and verify the result in supported browsers.

This rule tells the browser to preserve the specified aspect value for the selected metric. It doesn't assign a manually chosen CSS font-size to each fallback or define a new face. Keep the original text role's intended size and line height visible during testing so you can identify which declaration changed the result.

For an x-height decision, test representative lowercase text. Use uppercase labels for a cap-height decision and width-sensitive strings for a width decision. Include punctuation, numerals, controls, and every script the product supports. A paragraph of repeated Latin lowercase letters is too narrow a fixture for a product UI.

Recipe 2: define a metric-matched local fallback face

Use an adjusted face when a specific local font needs to stand in for a specific primary face. Give it a distinct family name. The project can then request the adjusted definition instead of bypassing it and selecting the raw local family.

@font-face {
  font-family: "Primary Sans Fallback";
  src: local("System Fallback");
  size-adjust: 101.5%; /* illustrative only */
  ascent-override: 91%; /* illustrative only */
  descent-override: 24%; /* illustrative only */
  line-gap-override: 0%; /* illustrative only */
  font-weight: 400;
  font-style: normal;
}

.product-body {
  font-family: "Primary Sans", "Primary Sans Fallback", sans-serif;
}
Illustrative adjusted-face structure. None of the percentages are universal or validated for a named font pair.

size-adjust scales outlines and metrics. The ascent, descent, and line-gap descriptors address the vertical proportions used for line layout. Tune them against the same primary face, weight, and style that the rule is meant to replace. A value derived from regular roman text doesn't automatically cover bold, italic, or a variable-font axis position.

Create separate face definitions when separate resources or measured variants require them. Make font-weight and font-style explicit. Otherwise, the browser may synthesize a requested face or select another available face, and you'll end up tuning numbers against the wrong comparison.

Keep an unadjusted control

Render the raw fallback, adjusted fallback, and loaded primary in the same fixture. Without the raw control, you can see that two samples differ, but you can't tell whether an override improved the intended metric.

Keep adjacent font decisions separate

Metric adjustment is only one part of a font implementation. It doesn't choose resources, loading behavior, or cascade mappings. Record those decisions separately so one layer's failure isn't hidden behind another metric tweak.

  • Family selection: which fallback is acceptable for each role, script, and environment.
  • Face mapping: which files or local faces serve each requested weight, style, stretch, and variable axis range.
  • Synthesis policy: whether the browser may synthesize bold or italic when a requested face is absent.
  • Loading policy: the font-display value and the resulting block and swap behavior.
  • Delivery: URLs, WOFF2 files, origin policy, subsetting, caching, and any justified preload.
  • Metric adjustment: property values or face-specific descriptors derived from the selected resources.
  • Rendered acceptance: observed glyphs, wrapping, line boxes, movement, and browser conditions.

Changing font-display can alter when the fallback and primary faces appear. Preloading can alter resource timing. Neither step makes two faces metrically compatible. The reverse is also true: a carefully adjusted fallback doesn't prove that the browser requested the primary resource successfully. Diagnose each stage separately.

Start from an explicit typography role

A design kit can establish the intended family, weights, scale, and usage rules before the consuming project chooses resources and fallback metrics. Use the public Ambient Sage typography system as bounded upstream input, then verify browser behavior inside your project.

Ambient Sage is upstream intake, not a browser result

Ambient Sage provides a concrete typography contract. Plus Jakarta Sans serves body and heading roles at weights 400, 500, 600, and 700. JetBrains Mono serves the mono role at weights 400, 500, and 700. Its published scale is compact-product. Those facts define what a consuming project is trying to implement, but they don't supply a universal system fallback, measured adjustment percentages, a loading policy, or cross-browser results.

Token specimen · real values

Ambient Sage

Live render

Ambient Sage's actual tokens — the same values its exports use.

Type scaleHeading, body, and mono in the kit's fonts

Typography

Ambient Sage

Scale: compact-product

Density: balanced

Heading · Plus Jakarta Sans · 1.875rem

Ship beautiful product faster

Subheading · Plus Jakarta Sans · 1.375rem

A warm-sage neutral-surface mobile kit with a single vivid yellow accent, flat tonal cards, and oversized display numerals.

Body · Plus Jakarta Sans · 1rem

Ambient Sage uses a near-white warm-sage canvas (#f3f4ef) with card panels distinguished only by a tonal shift to #e5e6e0, never by shadows or borders. A single vivid yellow (#fee951) is the only saturated color and appears sparingly at component scale as orbs, button fills, and focus rings. Primary data values render as oversized bold hero numerals with a small superscript unit. Typography is a friendly rounded geometric (Plus Jakarta Sans) with no uppercase and no tight tracking, while JetBrains Mono is reserved for hex codes and technical strings. Generous rounding and luminance-only contrast give the whole system a calm, minimal feel.

Mono · JetBrains Mono · 0.8125rem

npx shadcn add ambientsage.json

Aa

Plus Jakarta Sans · Heading

400500600700

Aa

Plus Jakarta Sans · Body

400500600700

ABCDEFGHIJKLM NOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz

0123456789 & @ # % →

The published Ambient Sage type roles are an upstream specification. The consumer still owns fallback selection, font files, metric overrides, and observed rendering.

Verify forced-fallback and loaded-font states

Generated CSS is evidence of a proposed implementation, not its effect. Test one named consumer in both font states. A high-reach body role, navigation label, or dense data row is a better starting point than an isolated alphabet sample because it exposes wrapping and neighboring layout changes.

  1. 1

    Freeze the consumer and conditions

    Name the route, component, text role, content fixture, viewport widths, browser versions, operating systems, zoom, and expected result. Record the current font files and CSS revision.

  2. 2

    Confirm the requested face

    Record the computed family, size, weight, style, stretch, line height, synthesis policy, and any font-size-adjust value. A request for weight 600 isn't proof that a 600 face rendered.

  3. 3

    Inspect the actual resource

    Use browser font and network inspection to identify the rendered face and determine whether the primary resource was requested, loaded, blocked, cached, or substituted. Preserve a screenshot or trace reference without treating it as evidence for other browsers.

  4. 4

    Force fallback

    Block the primary face or make it temporarily unavailable in the controlled test. Capture representative glyphs, line breaks, element dimensions, line boxes, overflow, truncation, and neighboring movement.

  5. 5

    Restore the loaded face

    Repeat the same fixture with the primary face confirmed as rendered. Compare the same measurements instead of relying on memory or a quick visual toggle.

  6. 6

    Test the swap path

    Observe whether and when the text changes under controlled loading conditions. Record layout movement separately from static similarity. A close final overlay doesn't prove that the loading transition was stable.

  7. 7

    Repeat the variants that can diverge

    Check required weights, italics, controls, numerals, scripts, and both narrow and wide viewports. Stop at the declared scope. One passing role doesn't approve the whole typography system.

RecordWhat to capture
RequestRequested family, weight, style, stretch, size, line height, and synthesis policyComputed CSS plus the rule and revision that supplied it
ResourceActual rendered face and primary resource statusBrowser font inspection and network evidence
Forced fallbackGlyphs, wrapping, line count, line boxes, overflow, and element dimensionsScreenshot plus recorded measurements at named viewports
Loaded faceThe same observations with the primary face confirmedMatching screenshot and measurements
Swap behaviorTiming and visible movement during the transitionControlled trace, layout-shift evidence, or recorded observation
DecisionPass, revise, block, or unresolved within the named scopeOwner, correction layer, evidence link, and retest trigger
A compact verification matrix for one named consumer.

A visual overlay is useful evidence, especially while tuning an adjusted fallback, but it remains incomplete. It may reveal outline or box differences in one fixture without showing which face rendered in the application, whether a resource failed, or whether a delayed swap moved the surrounding content.

Find the first divergent layer

When the result fails, correct the earliest layer that differs from the recorded expectation. Tuning another percentage at the end of the chain may hide the symptom while leaving the wrong face or resource mapping in place.

  1. Typography source: the intended family, role, weight, style, or acceptance criterion is missing or disputed.
  2. Font resource: the expected file or local face is absent, blocked, mislabeled, or different from the measured source.
  3. Emitted CSS: the intended property, descriptor, or face definition is missing or stale.
  4. Project mapping: the role points to the wrong family name, weight, style, variable range, or fallback order.
  5. Component override: local CSS changes the family, size, line height, letter spacing, or synthesis behavior.
  6. Rendered observation: the expected face renders, but wrapping, line boxes, legibility, or movement still misses the acceptance criterion.

A layout-shift clue is not a root cause

Layout-shift attribution can identify the element that moved without identifying the upstream element or font decision that caused the movement. Confirm the font state and compare line breaks before assigning the fix.

Copy the acceptance record

Write the expectations before testing and the observations afterward. This keeps a plausible screenshot from quietly changing the success criterion.

Font fallback acceptance record

Consumer: [route / component / text role]
Revision: [application and CSS revision]
Environment: [browser, version, OS, viewport, zoom]
Content fixture: [exact representative strings or fixture ID]

Requested family: [value]
Requested weight / style: [value]
Actual primary resource: [file or unresolved]
Actual fallback face: [face or unresolved]
Metric mechanism: [font-size-adjust / adjusted @font-face / neither]
Adjustment source: [measurement method and source/target face versions]

Expected forced fallback:
- representative glyphs: [criterion]
- wrapping and line count: [criterion]
- line boxes and overflow: [criterion]

Observed forced fallback: [unrun / observation]
Evidence: [reference]

Expected loaded face: [criterion]
Observed loaded face: [unrun / observation]
Evidence: [reference]

Expected swap movement: [criterion]
Observed swap movement: [unrun / observation]
Evidence: [reference]

First divergent layer: [source / resource / CSS / mapping / component / render]
Correction owner: [name or team]
Retest trigger: [resource, CSS, browser, content, or component change]
Disposition: [pass / revise / block / unresolved]
Scope limit: [what this record does not prove]
Leave unexecuted fields marked as unrun or unresolved. Don't convert missing observations into a pass.

Begin with one high-reach text role in one named consumer. Compare its raw fallback, adjusted fallback, loaded primary, and swap path. Keep the adjustment only when the recorded evidence shows that it improves the original failure without creating a worse result in another required state.

Sources