Block themes and theme.json
Presets and Style Variations
Presets are the vocabulary you hand the editor, style variations are alternate versions of it, and deciding what belongs in each layer is the work.
Two complaints arrive from opposite directions once a block theme is in use. The editor says your theme is restrictive, because the colour picker offers six options and no way to type anything else. The developer says the theme is too flexible, because users keep producing pages that look nothing like the design.
Both come from the same decision: what you put in a preset. A preset is a named value you are willing to support. It appears in the editor as a choice, it generates a custom property, and it is the only thing a style variation can redefine. Everything you leave out is either custom-value territory or a gap in your palette that someone will fill with a hex code.
A preset is a name you agree to keep
Presets are the entries inside settings: a colour in
settings.color.palette, a size in
settings.typography.fontSizes, a step in
settings.spacing.spacingSizes, a stack in
settings.typography.fontFamilies, a width in settings.layout.
Each one has a slug and a human name, and the slug is the part that matters. It is the
contract: accent will be the green, this year and next, whatever the hex
becomes.
That is what makes presets different from a CSS variable you invented. WordPress publishes them, the editor reads them, the block supports API offers them in the sidebar, and style variations can replace their values without any file being edited. A preset is a public API for your design, with the usual consequence that renaming one is a breaking change for anyone reading the old name in a plugin or a saved pattern.
So choose slugs you can defend. primary sounds fine until the brand
refresh swaps which colour is primary. accent, base,
contrast, muted describe roles rather than positions, and they
survive a redesign. That is the pattern the stock block themes settled on.
You can see the contract in the printed CSS. Each preset also generates a class, and the class is what an editor choice stores in the markup rather than a literal value, which is why changing the hex later moves every block that picked it.
a:where(:not(.wp-element-button)) {
color: var(--wp--preset--color--accent);
}
.has-accent-color { color: var(--wp--preset--color--accent) !important; }
.has-base-background-color {
background-color: var(--wp--preset--color--base) !important;
}
Disabling the defaults removes WordPress's palette, not yours
Out of the box, WordPress injects its own palette and font sizes alongside whatever your theme declares. A theme that lists four colours still shows a longer list in the picker, and the extras are core defaults that have nothing to do with your design. Two settings switch them off.
{
"version": 3,
"settings": {
"color": {
"defaultPalette": false,
"defaultGradients": false,
"defaultDuotone": false,
"palette": [
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#14161a", "name": "Contrast" },
{ "slug": "accent", "color": "#1f6f5c", "name": "Accent" }
]
},
"typography": {
"defaultFontSizes": false,
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "medium", "size": "1.125rem", "name": "Medium" },
{ "slug": "x-large", "size": "2rem", "name": "Extra Large" }
]
},
"spacing": {
"defaultSpacingSizes": false,
"spacingSizes": [
{ "slug": "30", "size": "0.5rem", "name": "Tight" },
{ "slug": "50", "size": "1.5rem", "name": "Normal" },
{ "slug": "70", "size": "3rem", "name": "Loose" }
]
}
}
}
Each of those flags has a precise effect, and it is stronger than it sounds. With the
defaults enabled, WordPress merges its own presets with yours, which is why a four-colour
palette still shows a long list in the picker. Set the flag to false and the
merge stops: what the editor sees is exactly your list, and core's classes stop being
printed into the CSS as well.
That last part is the cost. A post written before the change still carries the class in its saved markup, but the rule that class referred to no longer exists, so the element falls back to whatever it inherits. Text that was red becomes body text. Nothing errors and nothing warns; the page just renders differently from the screenshot the client approved. Audit for core preset classes in post content before you ship the flag, not after.
The other side of the same coin is that you now own the whole vocabulary. Disabling
defaultPalette means a block pattern copied from the WordPress pattern
directory, written against core colours, will show unset or broken colours in the editor,
and plugins that assume has-vivid-red-background-color exists will render
wrong. If you are distributing a theme, weigh the sharper design against that friction.
Most themes that disable the defaults also provide a full palette, so there is always a
reasonable option for whatever the user is trying to express.
A style variation is a partial theme.json in styles/
A style variation is a JSON file in the theme's styles/ directory, and it
is scanned recursively, so subdirectories work. It contains the same shape as
theme.json minus the parts that make no sense as an override. Give it a
title and it appears in the Site Editor's Styles screen as an option under
Browse styles.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Midnight",
"settings": {
"color": {
"palette": [
{ "slug": "base", "color": "#0f1115", "name": "Base" },
{ "slug": "contrast", "color": "#f4f4f2", "name": "Contrast" },
{ "slug": "accent", "color": "#7fd1b9", "name": "Accent" }
]
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
}
}
}
The file above redefines three palette slugs and reasserts the two style values that
depend on them. Everything it does not mention is inherited from
theme.json, which is why variations are small. You do not restate the
spacing scale, the font sizes or the layout widths; you change the palette and leave the
structure alone.
The slug key is optional. If you omit it, WordPress derives it from the
filename, which is the easier thing to maintain because the two cannot disagree. Files in
styles/ are also how block-level variations are declared, and the
distinguishing marker is a blockTypes key: with it the file is offered for a
specific block, without it the file is a theme style variation. A file that sets
blockTypes will not appear in the theme's Browse styles list, which is a
common five minutes of confusion.
styles is for CSS that has to exist, not for a fourth palette
The last layer is the theme's own CSS. In a block theme that means the
styles.css key inside theme.json, which takes a string of plain
CSS and prints it after the generated rules, plus style.css for anything that
has no place in the JSON.
Reach for styles.css when you need a selector that no theme.json property
expresses: a grid definition on a specific wrapper, a print stylesheet, a focus ring that
has to survive a plugin's reset. Reach for a preset when the editor should choose the
value. Reach for a variation when the whole set of choices should change at once, and for
a child theme when the change needs files rather than values.
| Decision | Preset in theme.json | Style variation | Child theme |
|---|---|---|---|
| Defines values | Yes, the base set | Overrides some of them | Overrides or replaces them |
| Changes templates or parts | No | No | Yes |
| Can add PHP | No | No | Yes |
| Who applies it | Nobody, it is the baseline | The site owner, from the Styles screen | Whichever theme is active |
| Reversible at runtime | n/a | Yes, reset restores the theme default | Only by switching themes |
A variation is a suggestion; a child theme is a decision. That distinction is the practical one. Ship three variations and the site owner can try a dark palette on a Friday and reset it on Monday without losing anything, because the variation was never destructive. Ship a child theme instead and you have taken on the maintenance of a second directory, a parent version bump that can break your overrides, and a deployment. Child themes are right when you need a different template, an extra block style or PHP that the parent does not offer. They are the wrong tool for changing four hex values.
Add two style variations to the theme you built in the previous lessons. The first,
styles/midnight.json, redefines the palette with a dark base and a light
contrast, matching the file above but with your own hex values. The second,
styles/editorial.json, leaves the palette alone and changes only
typography: a different styles.typography.fontSize, a
styles.elements.heading.typography.letterSpacing value, and a redefined
x-large font size preset.
Open the Site Editor, apply each variation in turn, and note which parts of the page changed and which stayed put. Then reset back to the theme default and confirm the original rendering returns.
Now test the two edges of the format. Add "blockTypes": ["core/heading"]
to a copy of editorial.json as styles/headings-only.json and
reload the Styles screen. Then write settings.color.defaultPalette: false
into theme.json, publish a post that uses a core colour such as Vivid red
first, and inspect that post's markup and its computed colour before and after the
change. Describe what stops being printed and what the element falls back to.
Show the solution and the reasoning
Both variations are small, and that is the correct outcome. If a variation restates the whole file, it will drift out of sync the next time the base theme changes.
styles/midnight.json touches the palette and the two style values that
read from it, and nothing else. The spacing scale, the font sizes and the layout widths
survive unchanged.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Midnight",
"settings": {
"color": {
"palette": [
{ "slug": "base", "color": "#101418", "name": "Base" },
{ "slug": "contrast", "color": "#eceff1", "name": "Contrast" },
{ "slug": "accent", "color": "#63c9a8", "name": "Accent" }
]
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
}
}
}
styles/editorial.json leaves colour entirely alone and changes scale and
rhythm. Redefining the x-large preset is the interesting part: it proves the
preset is a named indirection rather than a value, because every block that chose
"Extra Large" moves with it.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"title": "Editorial",
"settings": {
"typography": {
"fontSizes": [
{ "slug": "small", "size": "0.8125rem", "name": "Small" },
{ "slug": "medium", "size": "1.0625rem", "name": "Medium" },
{ "slug": "x-large", "size": "2.75rem", "name": "Extra Large" }
]
}
},
"styles": {
"typography": {
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.7"
},
"elements": {
"heading": {
"typography": { "letterSpacing": "-0.01em" }
}
}
}
}
Applying Midnight changes backgrounds, text and links, and leaves the type scale
alone. Applying Editorial does the reverse. Reset restores the base rendering because
the user's choice was stored as an override on top of theme.json, so
clearing it reveals the file again, unmodified. Nothing in the theme directory was
touched at any point, which is the property that makes variations safe to offer to
someone else.
The blockTypes test demonstrates the scope split described earlier.
Adding "blockTypes": ["core/heading"] makes that file a block style
variation, offered in the heading block's sidebar, and it disappears from Browse styles.
The palette and typography inside it still apply, but only within headings, because a
block variation is scoped to the block rather than to the site. If you want both
behaviours, ship two files; there is no single file that is both.
Disabling defaultPalette is the change with a bite, and the bite lands
on existing content rather than on new content. The class in the saved markup is
untouched, because it is part of the post. What changes is the CSS: core's palette
classes are only printed while the default palette is in play, so
.has-vivid-red-color stops having a rule. The paragraph keeps its class,
loses its colour, and inherits body text instead. Your own palette is unaffected
throughout, which is the part people get wrong when they expect the whole picker to
empty.
Two things complicate that. A colour chosen from the custom picker survives, because the value was written into the block as an inline style and no preset rule was ever involved. And a site that re-enables the flag brings the core classes back, so this is recoverable; it is just not something you want to discover from a support ticket.
The decision rule that falls out of all this: a value belongs in a preset if the editor should be able to choose it and you are willing to keep the slug stable. It belongs in a variation if it only makes sense as part of an alternative whole, such as a dark palette or a denser type scale. It belongs in the theme's own CSS if no editor control should ever touch it, which covers structural layout, print rules and accessibility affordances. Anything that needs a different template, an extra block style or PHP needs a child theme, and no amount of JSON will substitute.
Progress
Mark this lesson done and it stays marked in this browser.