theme.json
theme.json configures a theme's editor settings and default styles in one file, and generates CSS custom properties from its presets.
themes/{slug}/theme.json → { "version": 3, "settings": {…}, "styles": {…} }theme.json sits in the theme root and configures the editor and the site's default styles in one place, replacing a scatter of add_theme_support() calls and hand-written CSS. Both classic and block themes can use it.
settings vs styles
settings decides what the editor offers: which colours appear in the palette, which font sizes, whether padding controls are shown. styles decides what the site looks like by default — it compiles to real CSS output on the front end and in the editor.
Presets become custom properties
Every preset in settings generates a CSS variable named --wp--preset--{category}--{slug}: a palette entry with slug brand becomes --wp--preset--color--brand, a font size becomes --wp--preset--font-size--large, a spacing size --wp--preset--spacing--40. Anything under settings.custom becomes --wp--custom--*, with camelCase keys converted to kebab-case.
The version key
version is the schema version, not your theme's version. Version 2 works with WordPress 5.9 and later; version 3, the current one, was introduced in WordPress 6.6.
The cascade
Core's theme.json is merged with the theme's, then a selected variation from styles/, then the user's Global Styles — which are stored in the database and always win.
Example
<?php
/**
* Reading theme.json from PHP.
*
* Do not parse the file yourself: wp_get_global_settings() (WP 5.9+) returns
* the MERGED result of core, theme and user data, which is what actually
* applies on the page.
*/
add_action( 'wp_enqueue_scripts', function () {
// $path walks the settings tree: settings.color.palette
$palette = wp_get_global_settings( array( 'color', 'palette' ) );
/*
* Origins are stacked: 'default' (core), 'theme', 'custom' (the user's
* Global Styles). Ask for the theme's own palette specifically.
*/
$theme_colors = isset( $palette['theme'] ) ? $palette['theme'] : array();
$css = '';
foreach ( $theme_colors as $color ) {
// Each preset also exists as --wp--preset--color--{slug}; this builds a
// matching utility class without duplicating the values in CSS.
$css .= sprintf(
'.border-%1$s{border-color:var(--wp--preset--color--%1$s)}',
sanitize_html_class( $color['slug'] )
);
}
if ( $css ) {
wp_add_inline_style( 'mytheme-style', $css );
}
} );
// Pass $context to read a block's resolved settings rather than the globals.
$button_settings = wp_get_global_settings(
array( 'spacing' ),
array( 'block_name' => 'core/button', 'origin' => 'base' ) // 'base' = core + theme, no user data
);When to use it
- Locking a client's editor to an approved colour palette and type scale
- Setting the site's content and wide widths without writing layout CSS
- Shipping a dark variation of a block theme as styles/dark.json
- Reading a theme's resolved palette from PHP to generate matching utility classes
More examples
settings and styles side by side
settings.color.custom: false removes the arbitrary colour picker so users are held to the palette — a common brand-consistency requirement. typography.defaultFontSizes: false (version 3) hides core's own font sizes so only yours appear; in version 2 defining fontSizes replaced core's automatically, which is the behaviour change to watch for when migrating.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"color": {
"custom": false,
"palette": [
{ "slug": "brand", "color": "#0b5fff", "name": "Brand" },
{ "slug": "ink", "color": "#11151c", "name": "Ink" }
]
},
"typography": {
"defaultFontSizes": false,
"fluid": true,
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "large", "size": "1.75rem", "name": "Large" }
]
},
"layout": { "contentSize": "42rem", "wideSize": "64rem" },
"custom": { "lineHeight": { "tight": 1.1 } }
},
"styles": {
"color": { "text": "var(--wp--preset--color--ink)" },
"typography": { "lineHeight": "1.6" },
"elements": {
"link": { "color": { "text": "var(--wp--preset--color--brand)" } }
}
}
}Preset shorthand and the CSS it compiles to
Recognise both forms: hand-written theme.json tends to use the full var() call, while anything the Site Editor generates uses the var:preset|category|slug shorthand. They are interchangeable inside theme.json styles values.
{
"version": 3,
"styles": {
"elements": {
"button": {
"color": {
"background": "var:preset|color|brand",
"text": "var(--wp--preset--color--ink)"
}
}
}
}
}
/* Both lines above compile to the same declaration:
background-color: var(--wp--preset--color--brand);
color: var(--wp--preset--color--ink);
"var:preset|color|brand" is theme.json shorthand for the second form.
The shorthand is what the editor writes into block markup, so you will
meet it in serialised attributes as well as in theme.json. */Per-block styles and a style variation
styles.blocks scopes CSS to one block type without you writing a selector — core generates the correct .wp-block-quote rule and keeps it in sync if the markup changes. Files in styles/ reuse the same schema and are merged over theme.json when the user picks that variation in the Site Editor's Styles panel.
{
"version": 3,
"styles": {
"blocks": {
"core/quote": {
"typography": { "fontSize": "var(--wp--preset--font-size--large)" },
"border": { "left": { "width": "3px", "style": "solid", "color": "var(--wp--preset--color--brand)" } }
}
}
}
}
/* Saved as styles/dark.json, this becomes a selectable variation:
{
"version": 3,
"title": "Dark",
"settings": { "color": { "palette": [ { "slug": "ink", "color": "#f5f5f5", "name": "Ink" } ] } },
"styles": { "color": { "background": "#11151c" } }
}
*/
Discussion