Skip to content

fix(font): not emitting 400 as default value when font weight is omitted - #2796

Merged
james-elicx merged 4 commits into
cloudflare:mainfrom
NriotHrreion:fix/issue-2793-variable-fonts-clamping
Aug 13, 2026
Merged

james-elicx merged 4 commits into
cloudflare:mainfrom
NriotHrreion:fix/issue-2793-variable-fonts-clamping

Conversation

@NriotHrreion

@NriotHrreion NriotHrreion commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2793

Overview

The next/font/local shim emits font-weight: 400 when no weight was specified. For variable fonts with a wght axis, this descriptor clamps the font face to its regular 400 instance, preventing heavier weights from rendering correctly even when elements use font-weight: 600 or 700.

This aligns vinext with Next.js and the CSS Fonts specification by allowing the descriptor to retain its default auto behavior when omitted, while preserving explicit single weights, weight ranges, and source-level overrides. The fix applies consistently to both development and production output.

What changed

The original behavior sets font weight to 400 if weight is not provided

const weight = sanitizeFontDescriptorValue(src.weight ?? options.weight ?? "400") ?? "400";

rules.push(`@font-face {
font-family: '${escapeCSSString(family)}';
src: url('${escapeCSSString(src.path)}') format('${format}');
font-weight: ${weight};
font-style: ${style};
font-display: ${display};
}`);

The new behavior doesn't set font weight if weight is not provided, leaving it as auto

rules.push(`@font-face {
  font-family: '${escapeCSSString(family)}';
  src: url('${escapeCSSString(src.path)}') format('${format}');
- font-weight: ${weight};
- font-style: ${style};
+ ${weight === undefined ? "" : `  font-weight: ${weight};\n`}  font-style: ${style};
  font-display: ${display};
}`);

Testing

Corresponding tests are added or updated.

  • pnpm test tests/app-router-font-google-prod.test.ts tests/font-local-transform.test.ts tests/pages-router-font-google-prod.test.ts

Fixtures

Related fixtures (font-google-pages and font-google-multiple) are updated. Noto sans variable font is used in the fixtures because Noto sans makes the visual difference before and after this fix much more noticeable in the fixtures.

Before:
image

After:
image

Notes

I ran into this issue when migrating my project to vinext. Applying the fix is quite important to me. Thanks for reviewing!

…ted (cloudflare#2793)

Emitting `font-weight: 400;` when weight is omitted will clamp the wght axis of variable fonts
@NriotHrreion

Copy link
Copy Markdown
Contributor Author

@james-elicx

Hi! I ran into this issue while migrating my project from nextjs to vinext. The font issue can be quite noticeable after the migration, so I'd really appreciate it if you could take a look and review this PR when you have a chance. Thanks!

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues and parity

@pkg-pr-new

pkg-pr-new Bot commented Aug 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2796
npm i https://pkg.pr.new/create-vinext-app@2796
npm i https://pkg.pr.new/@vinext/types@2796
npm i https://pkg.pr.new/vinext@2796

commit: cba1b3a

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: font-weight descriptor omission

The core fix is correct and matches Next.js exactly. I verified against the Next.js canary nextFontLocalFontLoader (packages/font/src/local/loader.ts), which builds the @font-face descriptors with:

...((weight ?? defaultWeight) ? [['font-weight', weight ?? defaultWeight]] : []),

The PR's const configuredWeight = src.weight ?? options.weight; followed by conditional emission mirrors this precisely. Omitting the descriptor (rather than emitting 400) is the right behavior for variable fonts with a wght axis, and it aligns with the CSS Fonts spec auto default.

Two good things worth calling out:

  • The .className rule path (resolveSingleFaceStyle → formatFontClassRule) already omitted font-weight when unspecified (via resolveFontWeight returning undefined), so this fix correctly targets only the @font-face descriptor — no double-fix or divergence introduced.
  • Tests are well-constructed, cite the corresponding Next.js fixtures/loader, and all pass locally (font-local-transform, app-router-font-google-prod, pages-router-font-google-prod). Lint + fmt are clean.

Non-blocking observations

  1. Repo bloat from real 2MB TTFs. The two added noto-sans-wdth-wght.ttf files are identical (same md5) and ~2MB each, so this adds ~4MB to the repo. vinext's local-font shim never parses font bytes (no fontkit / no adjustFontFallback metric extraction) — the file is only served as a static asset and no assertion inspects its contents. A tiny stub or heavily subsetted .ttf would satisfy every test at a fraction of the size. Not blocking, but worth reconsidering before merge since these binaries live in git history forever.

  2. Orphaned stub fixture. See inline comment.

Nice, focused fix. Thanks for the thorough Next.js references in the test comments.

src: "./local.woff2",
src: [
{
path: "./noto-sans-wdth-wght.ttf",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixture no longer references ./local.woff2 after switching to noto-sans-wdth-wght.ttf. The 26-byte stub at tests/fixtures/font-google-multiple/app/local.woff2 is now orphaned and can be deleted.

(Note: tests/fixtures/font-google-pages/pages/local.woff2 must stay — it is still referenced by tests/pages-document-assets.test.ts:116.)

@ask-bonk

ask-bonk Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review complete and posted to PR #2796.

Summary

Verdict: The fix is correct and faithfully matches Next.js behavior. No blocking issues.

What I verified

  • Parity: Checked the fix against Next.js canary packages/font/src/local/loader.ts. Next.js only emits font-weight when weight ?? defaultWeight is truthy. The PR's configuredWeight = src.weight ?? options.weight + conditional emission mirrors this exactly. Omitting the descriptor (vs. emitting 400) is correct for variable fonts with a wght axis.
  • Targeting: The .className rule already omitted weight correctly (via resolveFontWeight → undefined), so this fix touches only the @font-face descriptor — no double-fix or divergence.
  • Tests: All three referenced test files pass locally; lint and fmt are clean. Tests cite Next.js fixtures/loader.
  • CSS output: Verified indentation is well-formed in both the weight-present and weight-absent branches.

Non-blocking feedback posted

  1. Repo bloat: The two added noto-sans-wdth-wght.ttf files are identical (~2MB each, ~4MB total). The shim never parses font bytes, so a tiny stub/subsetted font would satisfy every assertion.
  2. Orphaned fixture (inline): font-google-multiple/app/local.woff2 is now unreferenced and can be deleted. I noted that font-google-pages/pages/local.woff2 must stay (still used by pages-document-assets.test.ts:116).

github run

@james-elicx
james-elicx merged commit 98fd69c into cloudflare:main Aug 13, 2026
56 checks passed
@NriotHrreion
NriotHrreion deleted the fix/issue-2793-variable-fonts-clamping branch August 14, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

next/font/local emits font-weight: 400 when weight is omitted, clamping variable fonts

2 participants