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:
{
"$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: trueswitches on a group of design controls in the editor at once: border, padding and margin, minimum height, link colour and more.layoutsets the default width of content and of "wide" blocks.color.palettedefines three named brand colours.custom: falseremoves the free colour picker anddefaultPalette: falsehides WordPress's default colours, so editors can only choose brand colours.typography.fontFamiliesandfontSizesdefine the type scale. Theclamp()size grows smoothly with the screen width.defaultFontSizes: falsematters: WordPress has its own default sizes with the slugssmall,mediumandlarge, 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 withfontFaceentries pointing to font files in the theme.)
WordPress reads all this into its global settings:
<?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";flour = #fbf7f0
crust = #8a4b20
cocoa = #2b1d14
custom colours allowed: false
font sizes: small, medium, largeThe 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:
GET /--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-serifYour 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,typographyandspacingapply to the whole page (thebody); elementsstyle every link, heading, button, caption and so on;blocksstyle one block type everywhere, here the Site Title.
These become ordinary CSS rules:
<?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";
}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
customcolours on for a brand that must stay consistent. - Using an old
versionnumber; 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:
"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.