Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 19 of 45

Styling a theme with theme.json

Use theme.json to define a block theme's colour palette, fonts, font sizes, spacing and layout, style elements and blocks, and see the CSS it generates.

14 minBlock themesUpdated 23 October 2026

By the end of this lessonWrite a theme.json that gives your theme a consistent design system, controls which options editors see, and styles elements and blocks.

One file for the design system

theme.json is the heart of a block theme's design. It does two jobs:

  • settings: what design options exist (the colour palette, font families, font sizes, spacing scale, layout widths), and which options editors may use.
  • styles: how the site looks, using those options, for the whole site, for elements (links, headings, buttons) and for individual blocks.

From it, WordPress generates CSS, fills the colour and font pickers in the editor, and lets the Site Editor's Styles panel change the values. This lesson continues the Bakery theme from Your first block theme.

Settings: the design tokens

Replace the theme's theme.json with this fuller version:

json
{
	"$schema": "https://schemas.wp.org/trunk/theme.json",
	"version": 3,
	"settings": {
		"appearanceTools": true,
		"layout": { "contentSize": "680px", "wideSize": "1100px" },
		"color": {
			"custom": false,
			"defaultPalette": false,
			"palette": [
				{ "slug": "flour", "name": "Flour", "color": "#fbf7f0" },
				{ "slug": "crust", "name": "Crust", "color": "#8a4b20" },
				{ "slug": "cocoa", "name": "Cocoa", "color": "#2b1d14" }
			]
		},
		"typography": {
			"defaultFontSizes": false,
			"fontFamilies": [
				{ "slug": "body", "name": "Body", "fontFamily": "Georgia, 'Times New Roman', serif" },
				{ "slug": "heading", "name": "Heading", "fontFamily": "system-ui, sans-serif" }
			],
			"fontSizes": [
				{ "slug": "small", "name": "Small", "size": "0.875rem" },
				{ "slug": "medium", "name": "Medium", "size": "1.125rem" },
				{ "slug": "large", "name": "Large", "size": "clamp(1.5rem, 3vw, 2rem)" }
			]
		},
		"spacing": { "units": [ "px", "rem", "%" ] }
	},
	"styles": {
		"color": { "background": "var(--wp--preset--color--flour)", "text": "var(--wp--preset--color--cocoa)" },
		"typography": { "fontFamily": "var(--wp--preset--font-family--body)", "fontSize": "var(--wp--preset--font-size--medium)", "lineHeight": "1.6" },
		"spacing": { "padding": { "left": "1.5rem", "right": "1.5rem" } },
		"elements": {
			"link": { "color": { "text": "var(--wp--preset--color--crust)" } },
			"heading": { "typography": { "fontFamily": "var(--wp--preset--font-family--heading)" } },
			"button": { "color": { "background": "var(--wp--preset--color--crust)", "text": "var(--wp--preset--color--flour)" } }
		},
		"blocks": {
			"core/site-title": { "typography": { "fontSize": "var(--wp--preset--font-size--large)", "fontWeight": "700" } }
		}
	}
}

Going through settings:

  • appearanceTools: true switches on a group of design controls in the editor at once: border, padding and margin, minimum height, link colour and more.
  • layout sets the default width of content and of "wide" blocks.
  • color.palette defines three named brand colours. custom: false removes the free colour picker and defaultPalette: false hides WordPress's default colours, so editors can only choose brand colours.
  • typography.fontFamilies and fontSizes define the type scale. The clamp() size grows smoothly with the screen width. defaultFontSizes: false matters: WordPress has its own default sizes with the slugs small, medium and large, and while those defaults are enabled, a theme size with the same slug is ignored in favour of the default. Turning the defaults off makes our sizes the ones used. (Fonts can also be bundled with fontFace entries pointing to font files in the theme.)

WordPress reads all this into its global settings:

php
<?php
foreach ( wp_get_global_settings( [ 'color', 'palette', 'theme' ] ) as $colour ) {
	echo $colour['slug'], " = ", $colour['color'], "\n";
}
echo "custom colours allowed: ", var_export( wp_get_global_settings( [ 'color', 'custom' ] ), true ), "\n";
echo "font sizes: ", implode( ', ', wp_list_pluck( wp_get_global_settings( [ 'typography', 'fontSizes', 'theme' ] ), 'slug' ) ), "\n";
result
flour = #fbf7f0
crust = #8a4b20
cocoa = #2b1d14
custom colours allowed: false
font sizes: small, medium, large

The CSS WordPress generates

Each preset becomes a CSS custom property (a CSS variable) named --wp--preset--<type>--<slug>. They are printed in the page's <head>, which you can see in the page source:

http
GET /
result
--wp--preset--color--flour: #fbf7f0
--wp--preset--color--crust: #8a4b20
--wp--preset--color--cocoa: #2b1d14
--wp--preset--font-size--small: 0.875rem
--wp--preset--font-size--medium: 1.125rem
--wp--preset--font-size--large: clamp(1.5rem, 3vw, 2rem)
--wp--preset--font-family--body: Georgia, 'Times New Roman', serif
--wp--preset--font-family--heading: system-ui, sans-serif

Your own CSS, patterns and templates should use these variables instead of hard-coded values, so a colour change in one place changes the whole site. WordPress also generates helper classes such as .has-crust-color and .has-flour-background-color, which blocks add when an editor picks a palette colour.

Styles: applying the design

The styles section turns the tokens into a look:

  • top-level color, typography and spacing apply to the whole page (the body);
  • elements style every link, heading, button, caption and so on;
  • blocks style one block type everywhere, here the Site Title.

These become ordinary CSS rules:

php
<?php
$css = wp_get_global_stylesheet( [ 'styles' ] );
foreach ( [ '/body\{[^}]*background-color[^}]*\}/', '/a:where\(:not\(\.wp-element-button\)\)\{[^}]*\}/', '/[^}]*wp-block-site-title[^{]*\{[^}]*\}/' ] as $pattern ) {
	preg_match( $pattern, $css, $found );
	echo $found[0] ?? '(not found)', "\n";
}
result
body{background-color: var(--wp--preset--color--flour);color: var(--wp--preset--color--cocoa);font-family: var(--wp--preset--font-family--body);font-size: var(--wp--preset--font-size--medium);line-height: 1.6;padding-top: 0px;padding-right: 1.5rem;padding-bottom: 0px;padding-left: 1.5rem;}
a:where(:not(.wp-element-button)){color: var(--wp--preset--color--crust);}
:root :where(.wp-block-site-title){font-size: var(--wp--preset--font-size--large);font-weight: 700;}

Notice that the selectors are wrapped in :where() where needed, which gives them zero specificity, so a theme's or user's more specific CSS can easily override them.

Why not just write CSS?

You still can (in style.css, enqueued as the enqueueing lesson shows), and some things are easier in CSS. But theme.json has advantages:

  • the editor shows the same design as the front end;
  • editors pick from your palette and sizes instead of random values;
  • the Site Editor's Styles panel can change these values without code, and style variations (JSON files in a styles/ folder) can offer whole alternative looks;
  • WordPress only outputs the CSS that blocks on the page need.

Common mistakes

  • Invalid JSON: a trailing comma or missing quote makes WordPress ignore the whole file. Use an editor that validates against the $schema.
  • Hard-coding colours in templates and CSS instead of using the preset variables.
  • Leaving custom colours on for a brand that must stay consistent.
  • Using an old version number; this course uses version 3.

Exercise

Add a fourth palette colour, "Butter" (#f3d27a), and a styles.elements.h1 rule that makes level-one headings use the Crust colour. Check the page source for the new colour variable and the h1 rule.

Show one possible answer

Add { "slug": "butter", "name": "Butter", "color": "#f3d27a" } to settings.color.palette, and under styles.elements add:

json
"h1": { "color": { "text": "var(--wp--preset--color--crust)" } }

The page source then contains --wp--preset--color--butter: #f3d27a; and a rule such as h1{color: var(--wp--preset--color--crust);}. The Butter colour also appears in the editor's colour picker.

Key points

  • settings in theme.json define the design options: colour palette, font families and sizes, spacing scale and layout widths.
  • Every preset becomes a CSS custom property such as --wp--preset--color--crust, and appears in the editor's pickers.
  • styles applies the design: site-wide colours and typography, elements such as links and headings, and individual blocks.
  • Settings can also hide options, such as custom colours, to keep editors within the brand.

Check yourself

1. What does a palette colour with slug crust become in CSS?
2. What is the difference between settings and styles in theme.json?
3. How do you stop editors picking arbitrary colours?
4. How do you style every link on the site in theme.json?