theme.json

theme.json configures a theme's editor settings and default styles in one file, and generates CSS custom properties from its presets.

Syntaxthemes/{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

Example · php
<?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.

Example · json
{
  "$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.

Example · json
{
  "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.

Example · json
{
  "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

  • Be the first to comment on this lesson.