Skip to content

Typography: Add font-stretch and font-variation-settings support to Theme JSON styles. - #13789

Draft
Jiwoon-Kim wants to merge 3 commits into
WordPress:trunkfrom
Jiwoon-Kim:add/typography-font-stretch-and-variation-settings
Draft

Jiwoon-Kim wants to merge 3 commits into
WordPress:trunkfrom
Jiwoon-Kim:add/typography-font-stretch-and-variation-settings

Conversation

@Jiwoon-Kim

@Jiwoon-Kim Jiwoon-Kim commented Sep 27, 2026 •

Copy link
Copy Markdown

Why

Two typography style properties have no way to reach CSS today, and a variable font needs both. font-stretch is the one axis with a CSS property that a browser will not synthesize — there is no font-synthesis-width — so a width a font declares cannot be used at all without it. font-variation-settings is what the axes with no property of their own have, and there is no style property for it.

This is the Core half of editor work happening in Gutenberg: WordPress/gutenberg#83159 is the editor side, and WordPress/gutenberg#83148 is the design discussion behind it. This carries the contract only: what may be stored, what is written, and what is refused.

What

Stored under styles.typography Written as
fontStretch font-stretch
fontVariationSettings, an object keyed by axis tag font-variation-settings

Also: settings.typography.fontStretch beside the existing fontStyle and fontWeight; settings.typography.fontVariations, a boolean for whether a site offers custom-axis editing; axes on a font face, for the axes a file has and their ranges; font-variation-settings added to what safecss_filter_attr() allows, font-stretch being there already; and the two block supports.

How

WP_Style_Engine holds the rule for what may be written to font-variation-settings: a tag of four letters or digits, a numeric value, and not one of the four axes a CSS property owns (wght, wdth, slnt, ital), because the property is applied after those and would take the axis from them. opsz is allowed, since font-optical-sizing only switches the browser's tracking rather than taking a coordinate.

WP_Theme_JSON reads that rule through the style engine in both places it needs it — serializing a stylesheet, and sanitizing styles for a user without unfiltered_html — rather than holding a second copy. A second copy is what this whole line of work started from: two helpers reading the same font faces by different rules offered a static family nine widths it had no files for.

The block support's two lists of variables are regenerated rather than patched, because the longest name sets the column the coding standard aligns the assignments to. No line in them changes meaning.

Testing Instructions

  1. npm run test:php -- --filter 'Typography|Theme_JSON|Style_Engine|StyleEngine|ThemeJson|font_stretch'. 516 tests pass here.
  2. In a theme's theme.json, set styles.typography.fontStretch to "75%" and styles.typography.fontVariationSettings to { "GRAD": 50, "wght": 700 }. The stylesheet carries font-stretch: 75% and font-variation-settings: "GRAD" 50; the weight is left to font-weight, so a <strong> inside still renders bolder.
  3. As a user without unfiltered_html, save Global Styles with fontVariationSettings set to { "GRAD": 20, "wght": 700, "XTRA": "500; color: red" }. Only GRAD is stored.

Not included

The editor controls and the reading of a family's faces that decides what they offer, which stay in Gutenberg; which blocks opt in, since the supports here are off everywhere; and reading axes from a font file's fvar table when it is uploaded.

Nor wp-includes/theme.json, which turns the width setting on by default. The build copies that file from the plugin, so the default arrives with the Gutenberg pull request rather than from here; editing it in this branch was reverted by the build and caught by git diff --exit-code.

Trac ticket: https://core.trac.wordpress.org/ticket/66198

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Porting the implementation and tests from the Gutenberg prototype, and running the test suites. I reviewed the changes and the results, and one wrong assumption the port made was found by the tests rather than by reading: Core keys the font families setting by origin, as the plugin does.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Jiwoon-Kim and others added 3 commits September 28, 2026 01:06
Two typography style properties had no way to reach CSS. `font-stretch` is the
one axis with a CSS property that a browser will not synthesize, there being no
`font-synthesis-width`, so a width a variable font declares could not be used at
all. `font-variation-settings` is what the axes with no property of their own
have, and nothing could store a value for it.

Adds `styles.typography.fontStretch` and `styles.typography.fontVariationSettings`,
the latter an object keyed by axis tag so that the theme.json origins merge per
axis, where a string would replace the whole list. Adds the settings that go with
them, `axes` on a font face for the axes a file has and their ranges,
`font-variation-settings` to the properties `safecss_filter_attr()` allows, and
the two block supports.

The rule for what may be written to `font-variation-settings` lives in
`WP_Style_Engine`: a tag of four letters or digits, a numeric value, and not one
of the four axes a CSS property owns, because the property is applied after those
and would take the axis from them. `opsz` is allowed, since
`font-optical-sizing` only switches the browser's own tracking. `WP_Theme_JSON`
reads that rule through the style engine when it sanitizes rather than keeping a
second copy of it.

The two lists of variables in the block support are regenerated rather than
patched, because the longest name sets the column the coding standard aligns the
assignments to. No line in them changes meaning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six tests were written before the ticket existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`src/wp-includes/theme.json` is built rather than written: `copy:gutenberg-theme-json`
takes it from the Gutenberg plugin and rewrites only its `$schema`. Turning the
width setting on here was undone by the build and caught by the guard that
follows it, so the default arrives with the Gutenberg pull request instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@robelsust

robelsust commented Sep 27, 2026 •

Copy link
Copy Markdown

Tested PR #13789 / Trac #66198 on PHP 8.3 + macOS.

Test Results

  • PHPUnit: Ran the filtered typography suite (Typography|Theme_JSON|Style_Engine|StyleEngine|ThemeJson|font_stretch), passing all 516 tests (933 assertions).
  • PHPCS: 0 errors, 0 warnings across all 7 modified files.
  • Theme JSON generation: Verified that styles.typography.fontStretch produces font-stretch and fontVariationSettings compiles to font-variation-settings. Property-owned axes (wght, wdth, slnt, ital) are properly excluded from font-variation-settings.
  • Sanitization: Verified WP_Theme_JSON::remove_insecure_properties() strips invalid/injected values while preserving valid numeric axis declarations.
  • KSES: Verified safecss_filter_attr() allows font-variation-settings.

Minor Observation

In WP_Style_Engine::is_font_variation_axis( $tag, $value ):

return is_string( $tag )
    && 1 === preg_match( '/^[A-Za-z0-9]{4}$/', $tag )

Because PHP automatically casts numeric string array keys like '1000' to integers (int(1000)), is_string( $tag ) will return false for any valid 4-digit numeric axis tags passed as array keys. Checking ( is_string( $tag ) || is_int( $tag ) ) or (string) $tag would allow 4-digit tags to pass as intended.

Overall, the implementation and tests are very solid and ready for Core.


AI Disclosure

Assisted by generative AI tools to run automated checks (PHPUnit and PHPCS), execute edge-case scripts, and format the testing report.

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.

2 participants