4. theme.json: Design Tokens and Settings
There's a misconception that block themes put all their styles in theme.json. This is not the case, and fighting core stylesheet specificity is a losing battle.
The rule of thumb: theme.json is the source of truth for design tokens and settings. Actual styles belong in CSS files.
If you work in VS Code or Cursor, Sérgio Santos' WP Theme JSON Editor is worth installing. It's a form-driven visual editor for theme.json backed by the official WordPress schema: color pickers, spacing controls, CSS variable autocomplete for --wp--preset--* and --wp--custom--*, and block-level overrides without hand-editing raw JSON.
Learning Outcomes
- Know what belongs in
theme.json(tokens, settings, layout constraints) and what doesn't (actual CSS). - Understand the cascade: default → theme → user.
- Be able to add a new color, spacing size, or typography preset and use it in a template.
- Know how
theme.jsonvalues become CSS custom properties.
The cascade
WordPress resolves theme.json values in three layers:
- Default - WordPress core ships built-in presets (default palette, default spacing, etc.)
- Theme - Your
theme.jsonoverrides or extends the defaults - User - Changes made in the Site Editor's Global Styles panel override the theme
This means an editor can always override your theme's tokens via the Site Editor. That's by design. The theme establishes sensible defaults, and editors customize from there.
Copy theme.json from the finished theme
Rather than hand-authoring each section, copy the finished theme.json from the fueled-movies answer-key theme. From the wp-content directory, run:
cp themes/fueled-movies/theme.json themes/10up-block-theme/theme.json
No build needed: theme.json is read directly by WordPress. Refresh the Site Editor and the site should be ready to go dark with yellow accents.
Comparing the two themes
The 10up-block-theme ships a minimal theme.json: spacing presets, layout widths, a system font stack, and some viewport-aware calculations. The color palette is intentionally empty.
The fueled-movies theme.json, which now lives in your 10up-block-theme/theme.json after the copy, builds on that foundation significantly. Let's walk through the key additions.
Color palette
{
"settings": {
"color": {
"defaultPalette": false,
"palette": [
{
"slug": "yellow-primary",
"color": "var(--wp--custom--color--yellow--primary)",
"name": "Yellow Primary"
},
{
"slug": "yellow-secondary",
"color": "var(--wp--custom--color--yellow--secondary)",
"name": "Yellow Secondary"
},
{
"slug": "text-primary",
"color": "var(--wp--custom--color--text--primary)",
"name": "Text Primary"
},
{
"slug": "background-primary",
"color": "var(--wp--custom--color--background--primary)",
"name": "Background Primary"
}
]
}
}
}
Notice the indirection: palette colors reference --wp--custom--* variables rather than hardcoded hex values. This keeps the actual values in one place (the settings.custom block) and lets the palette entries act as aliases. Change the custom property, and every palette usage updates automatically.
Setting defaultPalette: false removes WordPress's built-in colors from the editor picker. This ensures editors can only use your intentional palette.
Custom properties (settings.custom)
The settings.custom block is where you define arbitrary CSS custom properties. WordPress generates --wp--custom--* variables from the nested structure:
{
"settings": {
"custom": {
"color": {
"yellow": {
"primary": "#F5C518",
"secondary": "#D8C882"
},
"text": {
"primary": "#C3C3C3",
"secondary": "#a3a3a3"
},
"background": {
"primary": "#0E0E0E",
"secondary": "#1A1A1A",
"nav": "#080808aa"
}
}
}
}
}
This generates CSS custom properties like:
--wp--custom--color--yellow--primary→#F5C518--wp--custom--color--text--primary→#C3C3C3--wp--custom--color--background--primary→#0E0E0E
You can use these anywhere: in CSS files, in theme.json style declarations, or via the editor's color controls.
Element-level styles
The fueled-movies theme defines default button and link styles at the element level:
{
"styles": {
"elements": {
"button": {
"color": {
"background": "var(--wp--preset--color--yellow-primary)",
"text": "#121212"
},
"border": {
"radius": "10px"
},
"shadow": "0 -2px 2px 0 rgba(0, 0, 0, 0.25) inset",
":hover": {
"color": {
"background": "var(--wp--preset--color--yellow-secondary)"
}
}
},
"link": {
"color": {
"text": "var(--wp--preset--color--yellow-secondary)"
}
}
}
}
}
These provide consistent defaults across all buttons and links in the theme. Individual blocks can still override them.
Notice we use --wp--preset--color--* here, not --wp--custom--color--*. The difference matters:
--wp--preset--color--*comes from the palette. When you use a preset reference instyles, the Site Editor knows which palette color you chose. An editor can override that color in Global Styles and the change flows through to buttons and links automatically.--wp--custom--color--*is a raw CSS variable. The Site Editor has no way to know it maps to a palette entry, so editor overrides in Global Styles won't affect it.
Use --wp--preset-- references in styles whenever the color is in your palette. Reserve --wp--custom-- for internal values that editors should not change, or for colors that are not exposed in the palette.
Avoid putting layout or visual styles in theme.json styles beyond element-level defaults. For anything more specific, use CSS. Core stylesheet specificity will fight you otherwise.
Spacing units
{
"settings": {
"spacing": {
"units": ["px", "em", "rem", "vh", "vw", "%"]
}
}
}
This controls which spacing units appear in the editor's dimension controls. The scaffold already includes fluid spacing presets using clamp(). These generate responsive values that scale between viewport widths.
Layout width
Update layout.wideSize from the scaffold's default to 1219px:
{
"settings": {
"layout": {
"wideSize": "1219px"
}
}
}
Custom templates
We already registered the four CPT-specific customTemplates entries and created placeholder template files in Lesson 3. No changes needed here.
How tokens become CSS
Every preset in theme.json generates a CSS custom property following a naming convention:
| Setting | CSS variable pattern | Example |
|---|---|---|
settings.color.palette | --wp--preset--color--{slug} | --wp--preset--color--yellow-primary |
settings.spacing.spacingSizes | --wp--preset--spacing--{slug} | --wp--preset--spacing--24 |
settings.typography.fontFamilies | --wp--preset--font-family--{slug} | --wp--preset--font-family--system-font |
settings.custom.* | --wp--custom--{path} | --wp--custom--color--yellow--primary |
The key difference: preset variables come from defined presets (palette, spacing sizes, font families). custom variables come from the settings.custom block. Both are equally usable in CSS, but presets also power the editor's UI controls (color picker, spacing controls, etc.).
Tasks
-
Verify. Refresh the Site Editor and confirm the custom palette appears in color pickers, the site is dark, and buttons are yellow with the inset shadow.
-
Test the cascade. Override one of your theme's colors in the Site Editor's Global Styles panel. Refresh and verify the user override wins. Reset it and verify the theme default returns.
Files changed in this lesson
| File | Change type | What changes |
|---|---|---|
theme.json | Modified | Added: 9-color palette, settings.custom with semantic tokens, styles.elements.button and styles.elements.link, spacing.units, layout.wideSize 1200px to 1219px |
Ship it checkpoint
- Site is dark with yellow accents
- Color pickers on blocks such as Heading show the custom palette with no default WordPress colors
- Buttons are yellow with inset shadow
- Updating a color in the Styles section of the Editor updates usage everywhere
Editing the palette from the Site Editor, noting how it changes our link color
If you experimented in the Site Editor's Global Styles panel, reset those changes to pick up the theme's defaults again. The copy you ran earlier only touches the theme file; it doesn't clear user overrides stored in the database.
Takeaways
theme.jsonis for tokens and settings. CSS is for styles.- Every preset generates a CSS custom property you can use anywhere.
settings.customcreates--wp--custom--*variables for anything you need.- The cascade is default, theme, then user. User overrides in the Site Editor win.
- Setting
defaultPalette: false(and similardefault*: falseflags) removes core presets so only your intentional choices appear. - Palette colors can reference custom properties for a single source of truth.
Further reading
- Theme.json Reference
- Styles Reference
- Remove settings in theme.json - what you can and cannot customize