theme.json Settings
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
theme.json is the configuration file of a block theme. Its settings object decides what the editors offer (which
colour, typography, spacing, border and layout controls appear, and which presets they list), and WordPress turns every
preset into a CSS custom property and, for some presets, a utility class. Its styles object applies values to the
site, elements and blocks, and is covered in theme.json Styles. This page is
for theme developers: it builds the settings of the Lumen Press block theme lumen and shows, from a running site,
what WordPress makes of them.
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), PHP 7.4 minimum and 8.3+ recommended, theme.json version 3 and WP-CLI 2.12.0, and checked
on 2026-10-28. The Gutenberg plugin (24.2.0) previews settings before they
reach core; nothing on this page requires it. WordPress 7.2 is in beta and due in December 2026; theme.json stays at
version 3 in it so far. Every merged value and CSS variable shown below was printed by wp eval on a fresh 7.1.3
installation with the themes of this page active, and each JSON file was validated against the published schema.
How a site owner changes these values without code is covered in
Site Editor and Global Styles; font families, fontFace and the
Font Library in Typography and Fonts; the styles/*.json files in
Style Variations and Section Styles.
What settings are for
A block theme describes its design system once, in theme.json, instead of spreading it across add_theme_support()
calls, editor stylesheets and front-end CSS:
-
Presets (palette, gradients, duotone filters, font sizes, font families, spacing sizes, aspect ratios, dimension sizes, shadows, border radius sizes): named values the editor lists in its pickers. A block that uses one stores the slug, not the value (
"backgroundColor":"lumen-red", orvar:preset|color|lumen-redin a style attribute), so changing the preset later restyles every block that uses it. -
Feature switches (
color.custom,typography.dropCap,spacing.padding,border.radius,position.stickyand so on): whether a control appears in the block sidebar at all. They only change the editing interface; values already saved in content keep rendering. -
Layout values (
layout.contentSize,layout.wideSize) used by the Group, Post Content and other layout blocks. -
Responsive breakpoints (
viewport, 7.1) used by responsive styles and block visibility. -
Custom properties (
custom) that become--wp--custom--*CSS variables for the theme’s own CSS and blocks.
Any of these can be set for the whole site or for one block type under settings.blocks. Settings never apply a style
by themselves: a palette colour colours nothing until a style or an editor uses it.
A theme does not need to be a block theme to use theme.json. A classic theme that ships one becomes a hybrid theme:
its editor gets the presets and controls, and since 6.8 it gets the Style Book and the Patterns screen under
Appearance > Design (see Hybrid Themes and
Migration to Block Themes).
The file: $schema and version
theme.json lives in the theme’s root folder. Two top-level keys matter before any setting:
version-
The schema version. It must be present: with no
versionkey WordPress replaces the whole file with an empty version 3 document, so every setting in it is silently ignored. Version 1 arrived in WordPress 5.8 (for classic themes and the post editor), version 2 in 5.9 with block themes, and version 3 in 6.6. 7.1 still uses version 3 (WP_Theme_JSON::LATEST_SCHEMAis3). Older files keep working:WP_Theme_JSON_Schema::migrate()upgrades them in memory each time they are read, without changing the file. $schema-
The URL of the JSON Schema, read only by editors (VS Code, PhpStorm and others) for autocompletion, descriptions and validation. WordPress ignores it at runtime.
https://schemas.wp.org/trunk/theme.jsonfollows the Gutenberg plugin’s development branch;https://schemas.wp.org/wp/7.1/theme.jsonmatches what WordPress 7.1 supports. Both redirect to theschemas/json/theme.jsonfile of the Gutenberg repository. Pin the version the theme declares inRequires at leastorTested up to: the trunk schema already lists settings that 7.1 does not apply (text-shadow presets andbackground.backgroundClip), so an editor would offer keys that do nothing on a 7.1 site.
Other top-level keys (styles, customTemplates, templateParts, patterns, title, slug, blockTypes) are
covered on the styles, templates and patterns pages.
The Lumen settings
The complete theme.json of the lumen theme, used for every result on this page. The theme also has the usual
style.css header (Text Domain lumen, Requires at least: 6.9) and templates/index.html, described in
Themes Overview. The styles object is reduced to the root padding that
useRootPaddingAwareAlignments needs; the theme’s real styles are on the styles page.
{
"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"useRootPaddingAwareAlignments": true,
"layout": {
"contentSize": "680px",
"wideSize": "1140px"
},
"viewport": {
"mobile": "600px",
"tablet": "1024px"
},
"color": {
"defaultPalette": false,
"defaultGradients": false,
"defaultDuotone": false,
"customGradient": false,
"palette": [
{ "slug": "paper", "name": "Paper", "color": "#fbf8f1" },
{ "slug": "ink", "name": "Ink", "color": "#1f1d1a" },
{ "slug": "lumen-red", "name": "Lumen red", "color": "#a3271f" },
{ "slug": "lumen-red-dark", "name": "Lumen red (dark)", "color": "#7a1d17" },
{ "slug": "mist", "name": "Mist", "color": "#e9e4d8" },
{ "slug": "slate", "name": "Slate", "color": "#4a4740" }
],
"gradients": [
{
"slug": "paper-to-mist",
"name": "Paper to mist",
"gradient": "linear-gradient(180deg, #fbf8f1 0%, #e9e4d8 100%)"
}
],
"duotone": [
{ "slug": "ink-and-paper", "name": "Ink and paper", "colors": [ "#1f1d1a", "#fbf8f1" ] }
]
},
"typography": {
"defaultFontSizes": false,
"fluid": {
"minViewportWidth": "360px",
"maxViewportWidth": "1140px"
},
"fontFamilies": [
{
"slug": "source-serif",
"name": "Source Serif",
"fontFamily": "\"Source Serif 4\", Georgia, serif"
},
{
"slug": "system-sans",
"name": "System sans",
"fontFamily": "system-ui, -apple-system, \"Segoe UI\", sans-serif"
}
],
"fontSizes": [
{ "slug": "small", "name": "Small", "size": "0.875rem", "fluid": false },
{ "slug": "medium", "name": "Medium", "size": "1.125rem", "fluid": { "min": "1rem", "max": "1.125rem" } },
{ "slug": "large", "name": "Large", "size": "1.75rem", "fluid": { "min": "1.375rem", "max": "1.75rem" } },
{ "slug": "x-large", "name": "Extra large", "size": "2.75rem", "fluid": { "min": "1.875rem", "max": "2.75rem" } }
]
},
"spacing": {
"defaultSpacingSizes": false,
"units": [ "px", "rem", "%", "vw" ],
"spacingSizes": [
{ "slug": "20", "name": "XS", "size": "0.5rem" },
{ "slug": "30", "name": "S", "size": "1rem" },
{ "slug": "40", "name": "M", "size": "clamp(1.25rem, 3vw, 2rem)" },
{ "slug": "50", "name": "L", "size": "clamp(2rem, 5vw, 3.5rem)" },
{ "slug": "60", "name": "XL", "size": "clamp(3rem, 8vw, 6rem)" }
]
},
"dimensions": {
"defaultAspectRatios": false,
"aspectRatios": [
{ "slug": "book-cover", "name": "Book cover", "ratio": "2/3" },
{ "slug": "wide", "name": "Wide", "ratio": "16/9" }
],
"dimensionSizes": [
{ "slug": "cover-s", "name": "Cover small", "size": "120px" },
{ "slug": "cover-m", "name": "Cover medium", "size": "200px" },
{ "slug": "measure", "name": "Reading measure", "size": "65ch" }
]
},
"shadow": {
"defaultPresets": false,
"presets": [
{ "slug": "card", "name": "Card", "shadow": "0 1px 3px rgb(31 29 26 / 0.15)" },
{ "slug": "lifted", "name": "Lifted", "shadow": "0 8px 24px rgb(31 29 26 / 0.2)" }
]
},
"border": {
"radiusSizes": [
{ "slug": "small", "name": "Small", "size": "2px" },
{ "slug": "medium", "name": "Medium", "size": "6px" },
{ "slug": "pill", "name": "Pill", "size": "9999px" }
]
},
"position": {
"sticky": true
},
"blockVisibility": {
"allowEditing": true
},
"custom": {
"lineHeight": {
"body": 1.6,
"heading": 1.2
},
"bookCard": {
"coverWidth": "var(--wp--preset--dimension--cover-m)",
"gap": "var(--wp--preset--spacing--30)"
}
},
"blocks": {
"core/button": {
"color": {
"palette": [
{ "slug": "lumen-red", "name": "Lumen red", "color": "#a3271f" },
{ "slug": "ink", "name": "Ink", "color": "#1f1d1a" }
]
}
},
"core/code": {
"typography": {
"customFontSize": false,
"fontSizes": [
{ "slug": "small", "name": "Small", "size": "0.8125rem", "fluid": false }
]
}
},
"core/image": {
"lightbox": {
"enabled": true,
"allowEditing": false
}
},
"core/paragraph": {
"typography": {
"dropCap": false
}
}
}
},
"styles": {
"spacing": {
"padding": {
"left": "var:preset|spacing|40",
"right": "var:preset|spacing|40"
}
}
}
}
The file validates against both the wp/7.1 and the trunk schema. Any JSON Schema draft-07 validator works; with
Node.js, ajv-cli checks it in one command (the schema must first be saved locally, because ajv-cli does not follow
the redirect):
curl -sL -o theme.schema.json https://schemas.wp.org/wp/7.1/theme.json
npx -y ajv-cli@5.0.0 validate --spec=draft7 --strict=false -s theme.schema.json -d wp-content/themes/lumen/theme.json
# wp-content/themes/lumen/theme.json valid
--strict=false is needed because the schema uses descriptive keywords that Ajv’s strict mode rejects. Validation
catches typos ("defaultPallete"), values of the wrong type and out-of-range numbers; it cannot tell whether a slug
collides with a core preset, which is what the migration section below is about.
Global switches
appearanceTools-
trueturns on a set of controls that core leaves off by default for themes. In 7.1 it enables background image, size and gradient; border colour, radius, style and width; link, heading, button and caption colours; aspect ratio, height, min-height, min-width and width; sticky position; block gap, margin and padding; line height; and text columns. The list grows with releases (width and height were added in 7.0,minWidthand background gradients in 7.1), so a theme that turns it on also opts into future controls. To keep one of them off, set that switch tofalsenext toappearanceTools, for instance"border": { "style": false }. After merging,wp_get_global_settings()reportsappearanceToolsasfalseand the individual switches astrue: the flag is expanded and then cleared. useRootPaddingAwareAlignments-
With
true, the root horizontal padding set instyles.spacing.paddingis applied to blocks that use thehas-global-paddingclass instead of to the whole page, and full-width children pull themselves back with negative margins. Full-width blocks then reach the screen edge while their content stays aligned with the rest of the page. It needs a padding value instylesto have any effect; Lumen uses the40spacing preset. layout-
contentSizeis the default width of the content in constrained layouts (the Post Content block, Groups with "Inner blocks use content width"), andwideSizethe width ofalignwideblocks. They become--wp--style--global--content-sizeand--wp--style--global--wide-size.allowEditing: falsehides the layout panel, andallowCustomContentAndWideSize: false(6.5) keeps the panel but hides the width inputs.
Colour
| Key | Effect |
|---|---|
|
The theme colours. Each entry has |
|
Gradient presets ( |
|
Two-colour image filters ( |
|
|
|
|
|
Show or hide each colour control. Link, heading, button and caption are off by default and turned on by |
Lumen hides core’s presets, adds six brand colours, one gradient and one duotone, and keeps custom colours for
editors but not custom gradients. A palette should be checked for contrast before release: WordPress warns about low
contrast in the block sidebar, but does not stop a combination such as slate text on ink
(Accessibility).
Typography
fontSizes presets generate --wp--preset--font-size--{slug} and the class has-{slug}-font-size. With fluid
turned on (true, or an object), each size becomes a clamp() that grows with the viewport:
-
the global
fluidobject setsminViewportWidthandmaxViewportWidth(the range over which sizes scale; core’s defaults are 320px and 1600px) andminFontSize, below which sizes are not made fluid; -
each preset may set
"fluid": { "min": ..., "max": ... }, or"fluid": falseto stay fixed; when a preset sets onlysize, WordPress derives a smaller minimum from it; -
since 6.7 a single preset may opt into fluid sizing even when the global setting is off.
Lumen’s medium size becomes
clamp(1rem, 1rem + ((1vw - 0.225rem) * 0.256), 1.125rem): at a 360px viewport 1vw equals 0.225rem and the size is
the minimum; it reaches the maximum at 1140px. The maths, font families, fontFace with self-hosted WOFF2 files and the
Font Library are covered in Typography and Fonts.
defaultFontSizes (6.6) decides what happens with core’s four sizes (small 13px, medium 20px, large 36px,
x-large 42px). The rest of the typography switches show or hide controls: customFontSize, dropCap, fontStyle,
fontWeight, letterSpacing, lineHeight, textAlign, textColumns, textDecoration, textIndent (7.0, which
core sets to "subsequent"), textTransform and writingMode.
Spacing
Spacing presets fill the spacing slider of padding, margin and block gap controls, and generate
--wp--preset--spacing--{slug}. There are two ways to declare them:
spacingScale-
A generated scale:
stepssizes around amediumStep, multiplied (operatorset to*) or added (+) byincrement, inunit. Core’s own scale is 7 steps from 0.44rem to 5.06rem, multiplied by 1.5 around 1.5rem, with the slugs20to80. spacingSizes-
An explicit list of
slug,nameandsize. Sizes may beclamp()expressions or references to other variables, so fluid spacing is written here.
Version 3 changed how the two combine. In version 2, a theme’s spacingSizes replaced its spacingScale entirely; in
version 3 they are merged: the scale generates its sizes, and spacingSizes entries are added, replacing generated
sizes with the same slug. The lumen-scale test theme below shows it (5 generated steps and two explicit entries):
{
"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
"version": 3,
"settings": {
"spacing": {
"defaultSpacingSizes": false,
"spacingScale": {
"operator": "*",
"increment": 1.5,
"steps": 5,
"mediumStep": 1.5,
"unit": "rem"
},
"spacingSizes": [
{ "slug": "gutter", "name": "Gutter", "size": "clamp(1rem, 4vw, 2.5rem)" },
{ "slug": "50", "name": "Medium (fixed)", "size": "1.75rem" }
]
}
}
}
Source: Theme Handbook: Spacing settings
theme spacingSizes: gutter, 30 (0.67rem), 40 (1rem), 50 (1.75rem), 60 (2.25rem), 70 (3.38rem)
The generated 50 (1.5rem) was replaced by the explicit one, and gutter was added in front. Lumen uses only
spacingSizes and leaves spacingScale out. Turning the scale off with "steps": 0, which some themes and tutorials
do, works at runtime but fails schema validation (steps has a minimum of 1); with defaultSpacingSizes set to
false and no theme scale, it is unnecessary.
Other spacing keys: units (the units offered in custom inputs), customSpacingSize (false limits editors to the
slider), and padding, margin and blockGap (show the controls; turned on by appearanceTools).
Dimensions, shadow, border, background and position
| Key | Since | Effect |
|---|---|---|
|
6.6 |
Aspect ratio presets for the Image, Cover and Featured Image blocks. Generates |
|
7.0 |
Size presets for the width, height and min-height controls. Generates |
|
6.2 to 7.1 |
Show the corresponding controls (all turned on by |
|
6.2 |
Box-shadow presets. Generates |
|
6.9 |
Radius presets for the border radius control. Generates |
|
5.9 |
Show the border controls. |
|
6.4, 6.5, 7.1 |
Show the background image, size and gradient controls. Core turns |
|
6.2 |
Show the sticky position control (Group block); |
|
6.4 |
"Expand on click" for the Image block, and whether editors may change it. |
|
7.1 |
|
Lumen sets lightbox under blocks.core/image rather than at the top level, because core’s own theme.json sets
lightbox.allowEditing to true for core/image at block level, and a block-level value from any layer wins over a
top-level value from a higher layer. A top-level "lightbox": { "allowEditing": false } therefore never reaches the
Image block’s sidebar.
Viewport breakpoints (7.1)
WordPress 7.1 adds responsive styles: styles under @mobile and @tablet keys, in theme.json or saved by the
editors, that apply only inside a viewport range (see theme.json Styles).
settings.viewport moves the two breakpoints:
| State | Default media query | Lumen (600px, 1024px) |
|---|---|---|
|
|
|
|
|
|
desktop (block visibility only) |
|
|
The rules, from WP_Theme_JSON::sanitize_viewport_settings():
-
values must be plain
px,emorremlengths (600px,37.5em);calc(),vw, percentages and unitless numbers are dropped, because the values are written into media queries; -
if only one value is valid it keeps its name and becomes a single max-width query: a
mobileof50vwwith atabletof40remyields only@tablet, atwidth <= 40rem; -
if
tabletis not larger thanmobile, onlymobileis kept; if neither is valid, the defaults apply; -
viewportis global: it cannot be set undersettings.blocks, and there is no@desktopstyle key (the default style is the desktop style).
The same breakpoints drive block visibility per viewport and the editor’s device preview. The checks above were run
with wp eval against the public helper:
wp eval 'print_r( WP_Theme_JSON::get_viewport_media_queries(
wp_get_global_settings( array( "viewport" ) ), array( "include_desktop" => true ) ) );'
Array
(
[@mobile] => @media (width <= 600px)
[@tablet] => @media (600px < width <= 1024px)
[@desktop] => @media (width > 1024px)
)
Custom properties
settings.custom accepts any nested object. WordPress flattens it into --wp--custom-- variables, joining levels with
-- and turning camelCase into kebab-case: custom.bookCard.coverWidth becomes --wp--custom--book-card--cover-width.
Values are copied as they are, so they may reference presets. Lumen keeps its line heights and the measurements of the
lumen/book-card block there, so the block’s stylesheet uses var(--wp--custom--book-card--cover-width) and a style
variation or child theme can change the card without touching the block.
Two cautions: camelCase conversion means bookCard and book-card produce the same variable, and numbers stay
unitless (1.6), so a value meant as a length needs its unit in the JSON.
Per-block settings
Every setting except viewport and blockVisibility can be repeated under settings.blocks.<block name> to change what
the editor offers for that block type only. Lumen:
-
limits the Button block to two colours (
lumen-red,ink); the variables are re-declared on the button’s selector (.wp-block-button .wp-block-button__link) and the colour classes are scoped to it; -
gives the Code block its own
smallsize (0.8125rem) and no custom sizes; the variable is re-declared on.wp-block-code, sohas-small-font-sizemeans a different size inside code blocks only; -
hides the drop cap for paragraphs;
-
turns the Image lightbox on and locks it.
Block settings of custom blocks work the same way (settings.blocks.lumen/book-card), and blocks can declare their
own defaults in block.json supports, which form the "blocks" layer below the theme (see
Attributes, Supports and InnerBlocks).
Layers: core, parent, child and user
WordPress never reads one theme.json. WP_Theme_JSON_Resolver::get_merged_data() builds the result from four
origins, each overriding the previous one, and each passes through a filter first:
Inside the theme origin, theme supports (add_theme_support( 'editor-color-palette' ) and similar, mostly used by
classic themes) are the lowest level, then the parent’s theme.json, then the child’s. The user origin is the
wp_global_styles post of the active theme, written by the Styles panel; choosing a style variation copies that
variation’s JSON into it.
Merging rules that matter in practice:
-
Objects merge key by key: a child that sets only
layout.wideSizekeeps the parent’scontentSize. -
Preset lists replace each other per origin: a child’s
palettereplaces the parent’s whole palette, it does not add to it. To add one colour, the child repeats the parent’s list. -
Presets are kept per origin, not flattened:
wp_get_global_settings( array( 'color', 'palette' ) )returnsdefault,themeandcustomlists, and the editor shows them as separate groups ("Default", "Theme", "Custom"). -
Block-level values stay block-level: the parent’s
settings.blockssurvive in the child unless the child overrides the same block and key. -
The
wp_theme_json_data_themefilter receives the child’s data when a child theme is active; the parent’s file is merged underneath afterwards.
The lumen-child theme overrides three colours and the wide size:
{
"$schema": "https://schemas.wp.org/wp/7.1/theme.json",
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "paper", "name": "Paper", "color": "#ffffff" },
{ "slug": "ink", "name": "Ink", "color": "#111111" },
{ "slug": "lumen-red", "name": "Lumen red", "color": "#8c1d17" }
]
},
"layout": {
"wideSize": "1280px"
}
}
}
Source: Theme Handbook: Child Themes
With the child active and one colour saved in the Styles panel (the sale colour, saved through the same
/wp/v2/global-styles/<id> endpoint the editor uses), a short script prints each origin:
<?php
// layers.php: prints the palette per origin and the merged layout widths.
$palette = wp_get_global_settings( array( 'color', 'palette' ) );
foreach ( $palette as $origin => $colors ) {
$pairs = array_map(
static function ( $c ) {
return $c['slug'] . '=' . $c['color'];
},
$colors
);
printf( "%-7s %s\n", $origin, implode( ' ', $pairs ) );
}
printf(
"contentSize %s, wideSize %s\n",
wp_get_global_settings( array( 'layout', 'contentSize' ) ),
wp_get_global_settings( array( 'layout', 'wideSize' ) )
);
$ wp eval-file layers.php
default black=#000000 cyan-bluish-gray=#abb8c3 white=#ffffff pale-pink=#f78da7 vivid-red=#cf2e2e ...
theme paper=#ffffff ink=#111111 lumen-red=#8c1d17
custom sale=#d4a017
contentSize 680px, wideSize 1280px
Source: WP-CLI: wp eval-file
The parent’s mist and slate are gone (the child’s list replaced them), contentSize still comes from the parent,
and the user colour is a separate group. The parent’s Button palette is also still in force, so inside buttons
--wp--preset--color--lumen-red keeps the parent’s #a3271f while the rest of the page uses the child’s #8c1d17. A
child theme that changes brand colours must therefore repeat the per-block palettes too. Child theme mechanics are
covered in Child Themes.
Changing settings from PHP
A plugin can change any layer with the four filters. Each receives a WP_Theme_JSON_Data object; update_with()
merges a partial theme.json array (with its version) into it. The must-use plugin below removes the free colour
picker and custom font sizes on every theme, which a publisher may want so that editors stay within the brand:
<?php
/**
* Plugin Name: Lumen theme.json guardrails
* Description: Locks colour and font-size choices to the theme presets on every theme.
*/
add_filter(
'wp_theme_json_data_theme',
static function ( WP_Theme_JSON_Data $theme_json ): WP_Theme_JSON_Data {
return $theme_json->update_with(
array(
'version' => 3,
'settings' => array(
'color' => array( 'custom' => false ),
'typography' => array( 'customFontSize' => false ),
),
)
);
}
);
Saved as wp-content/mu-plugins/lumen-theme-json-guardrails.php, it makes
wp_get_global_settings( array( 'color', 'custom' ) ) return false. A user-layer value would still win over the
theme layer, but the Styles panel cannot save a setting whose control it no longer shows. Results are cached per
request; code that changes settings after wp_get_global_settings() has run must call
wp_clean_theme_json_cache().
Version 2 to version 3
Version 3 changed defaults, not syntax. Upgrading means changing "version": 2 to 3 and checking three things:
| Behaviour | Version 2 | Version 3 |
|---|---|---|
Theme |
Override core’s sizes |
Ignored while |
Theme |
Override core’s sizes |
Ignored while |
Theme |
|
Merged |
The migration that WordPress applies to version 2 files in memory sets defaultFontSizes and defaultSpacingSizes to
false when the theme defines font sizes or spacing sizes, which preserves version 2 behaviour; a file that says
"version": 3 gets no such help. The difference shows with one fragment loaded once as version 2 and once as
version 3:
{
"version": 2,
"settings": {
"typography": {
"fontSizes": [
{ "slug": "small", "name": "Small", "size": "0.875rem", "fluid": false }
]
},
"spacing": {
"spacingSizes": [
{ "slug": "40", "name": "M", "size": "2rem" }
]
}
}
}
<?php
// probe.php: prints the effective flags and the variables for the colliding slugs.
$typography = wp_get_global_settings( array( 'typography' ) );
$spacing = wp_get_global_settings( array( 'spacing' ) );
printf(
"defaultFontSizes=%s defaultSpacingSizes=%s\n",
var_export( $typography['defaultFontSizes'], true ),
var_export( $spacing['defaultSpacingSizes'], true )
);
$css = wp_get_global_stylesheet( array( 'variables' ) );
preg_match_all( '/--wp--preset--(font-size--small|spacing--40):[^;]+/', $css, $m );
echo implode( "\n", $m[0] ), "\n";
== "version": 2
defaultFontSizes=false defaultSpacingSizes=false
--wp--preset--font-size--small: 0.875rem
--wp--preset--spacing--40: 2rem
== "version": 3
defaultFontSizes=true defaultSpacingSizes=true
--wp--preset--font-size--small: 13px
--wp--preset--spacing--40: 1rem
With version 3 the theme’s small size is dropped from the theme list altogether, so the editor shows core’s 13px
"Small" and every has-small-font-size paragraph shrinks. The fix is the one Lumen uses: set defaultFontSizes and
defaultSpacingSizes to false explicitly (or use slugs that do not collide). Style variation files in styles/ have
their own version and are migrated separately, so they must be upgraded together with theme.json. The other preset
types (palette, gradients, duotone, shadows, aspect ratios) already behaved this way in version 2 through their own
default* switches.
The generated CSS variables
wp_get_global_stylesheet() returns the CSS that WordPress prints in the global-styles-inline-css style tag and the
editor iframe. Its types argument selects parts: variables (custom properties), presets (the has-* classes),
styles and base-layout-styles. With lumen active:
wp theme activate lumen
wp eval 'echo wp_get_global_stylesheet( array( "variables" ) );' | sed 's/;/;\n/g; s/{/{\n/g; s/}/}\n/g'
Source: WP-CLI: wp eval
:root{
--wp--preset--aspect-ratio--square: 1;
/* ...the other six core aspect ratios... */
--wp--preset--aspect-ratio--book-cover: 2/3;
--wp--preset--aspect-ratio--wide: 16/9;
--wp--preset--color--black: #000000;
/* ...the other eleven core colours... */
--wp--preset--color--paper: #fbf8f1;
--wp--preset--color--ink: #1f1d1a;
--wp--preset--color--lumen-red: #a3271f;
--wp--preset--color--lumen-red-dark: #7a1d17;
--wp--preset--color--mist: #e9e4d8;
--wp--preset--color--slate: #4a4740;
/* ...twelve core gradients... */
--wp--preset--gradient--paper-to-mist: linear-gradient(180deg, #fbf8f1 0%, #e9e4d8 100%);
--wp--preset--font-size--small: 0.875rem;
--wp--preset--font-size--medium: clamp(1rem, 1rem + ((1vw - 0.225rem) * 0.256), 1.125rem);
--wp--preset--font-size--large: clamp(1.375rem, 1.375rem + ((1vw - 0.225rem) * 0.769), 1.75rem);
--wp--preset--font-size--x-large: clamp(1.875rem, 1.875rem + ((1vw - 0.225rem) * 1.795), 2.75rem);
--wp--preset--font-family--source-serif: "Source Serif 4", Georgia, serif;
--wp--preset--font-family--system-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--wp--preset--spacing--20: 0.5rem;
--wp--preset--spacing--30: 1rem;
--wp--preset--spacing--40: clamp(1.25rem, 3vw, 2rem);
--wp--preset--spacing--50: clamp(2rem, 5vw, 3.5rem);
--wp--preset--spacing--60: clamp(3rem, 8vw, 6rem);
--wp--preset--spacing--70: 3.38rem;
--wp--preset--spacing--80: 5.06rem;
--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);
/* ...the other four core shadows... */
--wp--preset--shadow--card: 0 1px 3px rgb(31 29 26 / 0.15);
--wp--preset--shadow--lifted: 0 8px 24px rgb(31 29 26 / 0.2);
--wp--preset--border-radius--small: 2px;
--wp--preset--border-radius--medium: 6px;
--wp--preset--border-radius--pill: 9999px;
--wp--preset--dimension--cover-s: 120px;
--wp--preset--dimension--cover-m: 200px;
--wp--preset--dimension--measure: 65ch;
--wp--custom--line-height--body: 1.6;
--wp--custom--line-height--heading: 1.2;
--wp--custom--book-card--cover-width: var(--wp--preset--dimension--cover-m);
--wp--custom--book-card--gap: var(--wp--preset--spacing--30);
}
.wp-block-button .wp-block-button__link{
--wp--preset--color--lumen-red: #a3271f;
--wp--preset--color--ink: #1f1d1a;
}
.wp-block-button{
/* core's 25%, 50%, 75% and 100% dimension presets */
}
.wp-block-code{
--wp--preset--font-size--small: 0.8125rem;
}
What the output shows:
-
The naming pattern is
--wp--preset--{type}--{slug}for presets and--wp--custom--{path}for custom values. Theme CSS and block stylesheets should reference these variables, never the raw values. -
Hidden defaults are still printed.
defaultPalette: falseremoves core’s colours from the pickers, not from the CSS, so old content that usesvivid-redkeeps rendering. -
Spacing
70and80come from core: withdefaultSpacingSizes: falsethe theme’s20to60win, but core’s sizes for slugs the theme does not define are still printed. The editor offers only the theme’s five. -
Per-block presets are re-declared on the block’s selector, which is how one slug can mean two values.
-
Duotone presets do not appear: their filters are output only for blocks that use them.
The presets part contains the utility classes, such as
.has-lumen-red-background-color{background-color: var(--wp--preset--color--lumen-red) !important;} and, for the Button
palette, the same classes wrapped in :where(.wp-block-button .wp-block-button__link). The layout values appear as
--wp--style--global--content-size: 680px and --wp--style--global--wide-size: 1140px in the styles part, and
useRootPaddingAwareAlignments produces the .has-global-padding rules there.
The merged settings themselves come from wp_get_global_settings(), with an optional path and a block_name or
origin context, as the scripts in the layers and migration sections show.
The site used for these results is the standard Compose stack (MariaDB 11.8 and the wordpress:cli-2.12.0-php8.4
image) from Local Development Environments, with the theme folder
copied into wp-content/themes/. Settings are read from the files on every request (with a per-request cache), so an
edit to theme.json shows on the next page load without reactivating the theme. On a production server with a
persistent object cache, the theme.json cache is cleared when the theme is switched (switch_theme calls
wp_clean_theme_json_cache()); after deploying a changed theme.json without a switch, run wp cache flush.
Security notes
-
Theme files are trusted code: whatever
theme.jsondeclares is printed into CSS without sanitizing, like the theme’s own stylesheet. Only install themes from trusted sources. -
The user layer is not trusted. When a user without
unfiltered_htmlsaves global styles, the JSON passes throughwp_filter_global_styles_post(), which removes unsafe values withWP_Theme_JSON::remove_insecure_properties()before it is stored. -
viewportvalues are restricted topx,emandrembecause they are interpolated into media queries; other values are discarded rather than escaped. -
Feature switches are interface limits, not permissions. A user with
edit_postscan still put any value in block markup through the code editor or the REST API; to restrict what content may contain, use capabilities, block locking andallowed_block_types_all, nottheme.json.
What older books and tutorials get wrong
| Older advice | Current state (7.1) |
|---|---|
Design settings are configured with |
|
Full site editing and its configuration date from WordPress 5.4 to 5.6 |
|
Site-wide design is "still maturing" and needs a commercial theme or page builder |
Presets, fluid type, responsive breakpoints and per-block settings cover what builders were used for; the trade-offs are discussed in Page Builders and Commercial Themes. |
Tutorials written for version 2: a theme defines |
In version 3 they are ignored until |
|
In version 3 the two are merged. |
|
Works at runtime but fails schema validation; set |
|
Pin the schema of the oldest supported WordPress version, such as |
Breakpoints in theme CSS with hard-coded media queries |
7.1 has |
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
No setting in |
The file has no |
Theme font sizes or spacing sizes are ignored after upgrading to version 3 |
The slugs collide with core’s. Set |
Core colours still appear in the CSS after |
Expected: the variables stay for existing content; only the pickers hide them. |
The child theme lost colours of the parent |
Preset lists replace each other; repeat the parent’s entries in the child. |
Buttons keep the old brand colour in a child theme |
The parent’s |
A top-level setting does not reach one block |
Core or a block’s |
|
|
A setting from a tutorial is accepted by the editor’s autocompletion but does nothing |
The |
A change to |
The user layer overrides it (reset Styles), a persistent object cache holds the old data ( |