Skip to content
System nominalUTC --:--:--Articles 041
MODULE 01 Lesson 03 / 06 30 MIN

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.

DecisionPreset in theme.jsonStyle variationChild theme
Defines valuesYes, the base setOverrides some of themOverrides or replaces them
Changes templates or partsNoNoYes
Can add PHPNoNoYes
Who applies itNobody, it is the baselineThe site owner, from the Styles screenWhichever theme is active
Reversible at runtimen/aYes, reset restores the theme defaultOnly 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.

Lab 30 minutes

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.