theme.json Explained
theme.json is one file that defines a block theme's entire design system: which controls editors get, and what those controls output as CSS. Fighting it with hand-written CSS usually means losing a specificity battle you didn't know you'd started.
What theme.json actually is: a single JSON file at a block theme's root that declares the design system the site editor exposes — the colours, fonts, spacing scale and layout rules available to editors, plus the base styles WordPress compiles into CSS. It is configuration, not a stylesheet, though most of it ends up as one.
Settings versus styles
The file has two top-level sections doing two different jobs, and mixing them up is the most common source of confusion.
settings controls what editors are allowed to do. It defines the colour palette that shows in colour pickers, the font sizes available in the typography controls, whether custom spacing is permitted, whether the duotone filter is offered on images. Settings do not, by themselves, style anything — they open or close doors in the editor UI.
styles applies actual values. Global text colour, the site's base font family, the default spacing between blocks, per-element and per-block defaults. This is the section that outputs CSS people can see on the front end without opening the editor at all.
A theme can define a colour palette in settings without ever using those colours in styles, and it can set a global font in styles that isn't listed as a settings option at all (though then no editor control lets someone else pick it). Keeping the two separated in your head is the fastest way to debug "why can't I change this" versus "why does this look wrong."
What it generates
WordPress reads theme.json at runtime and produces two things:
- CSS custom properties, one per setting, following a predictable naming pattern such as
--wp--preset--color--primaryor--wp--preset--spacing--40. Every colour, font size and spacing value declared insettings.presetsgets one. - Utility classes, matching those same presets —
has-primary-color,has-primary-background-color, and equivalents for font size and spacing. These are the classes blocks apply when an editor picks a preset value from a dropdown rather than typing a custom one.
{
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#1d3557", "name": "Primary" }
]
}
}
}
That single palette entry produces both --wp--preset--color--primary as a custom property and .has-primary-color / .has-primary-background-color as classes, without a line of CSS written by hand. This is why changing a colour's hex value in theme.json updates it everywhere the preset is used, but renaming the slug breaks every block already using the old class name — the class is generated from the slug, not from the colour.
Disabling controls you don't want editors touching
Every settings sub-section supports a custom flag and, for many, presence in settings at all is what gates the control. Turning custom off for typography removes the free-form font-size input and leaves only the preset dropdown, for example. This matters for client sites and multi-author blogs more than for a site you edit alone: it is the mechanism behind WordPress user roles actually meaning something when it comes to design consistency, since a role that can edit content can otherwise pick any colour or size it likes.
{
"settings": {
"color": {
"custom": false,
"customDuotone": false
},
"typography": {
"customFontSize": false
}
}
}
Locking things down like this is a deliberate trade-off: fewer support requests about off-brand colours, at the cost of flexibility for anyone doing something the palette didn't anticipate.
The cascade, in order
Four layers stack, each able to override the one before it:
- theme.json — the theme's base settings and styles.
- Global styles (the Styles panel) — a site owner's overrides, saved to the database on top of theme.json, the same layer used when customising a WordPress theme without editing files.
- Block-level styles — settings applied to one specific block instance in the content, via the block's own style controls.
- Custom CSS — anything added through adding custom CSS to WordPress, which loads after the generated stylesheet and, specificity aside, wins ties by load order.
Custom CSS being loaded last is why it usually — not always — beats theme.json on a plain property. But theme.json output frequently targets elements through the same custom-property mechanism rather than a plain property, and a custom-property override needs to happen at the right level of the cascade to take effect, which is the actual reason hand-written CSS sometimes appears to lose against theme.json even though it loads later.
Per-block styles
Beyond global defaults, theme.json accepts a styles.blocks object keyed by block name, letting a theme set different defaults per block without touching global settings:
{
"styles": {
"blocks": {
"core/button": {
"border": { "radius": "2px" },
"spacing": { "padding": { "top": "0.75rem", "bottom": "0.75rem" } }
}
}
}
}
This is how a theme gives buttons squared corners while leaving the rest of the design untouched, without a separate CSS file. It sits below global styles and above nothing further at the theme level — an editor's global-style override for buttons, made through the Styles panel, still takes precedence.
Common mistakes
- Writing CSS to fight generated rules instead of editing settings. If a theme's Styles panel already exposes the control, changing theme.json or global styles is more durable than a specificity war.
- Renaming a colour or font-size slug after it is in use. Every block referencing the old class name silently loses that styling.
- Confusing
settingsandstyles. Adding a colour to the palette without also referencing it instylesleaves defaults unchanged; it only becomes available as an option. - Assuming theme.json changes show immediately for every visitor. Cached pages and cached CSS need clearing, per how to clear WordPress cache.
- Hand-editing theme.json while global styles overrides already exist. The database layer still wins, so the file change appears to do nothing until the override is reset.
Verify
Change one palette colour's hex value in theme.json, reload the site editor, and confirm the swatch and every block using that preset updated together. Then open the Styles panel, override the same colour there, and confirm it now wins over the theme.json value — that is the cascade working as intended, not a bug. Finally check the front end in a private window to rule out cached CSS masking the change.
Frequently asked
- Technically no, a block theme can ship with a minimal one, but without it you lose most of the design-system benefits: no generated custom properties, no editor controls tuned to your palette, and no clean way to define style variations.
- theme.json output is compiled into a stylesheet with its own specificity, often boosted with custom-property-based selectors that are harder to out-rank than they look. Matching or exceeding that specificity, or targeting the same custom properties instead, usually works better than adding more rules.
- A theme has one theme.json, but style variations, defined as separate JSON files in a styles directory, layer additional settings and styles on top of it, and editors can switch between them from the Styles panel.