The editor showed a colour picker offering sixteen million colours on a site with a five-colour brand palette, and the resulting pages had eleven distinct shades of blue in them. The theme had a palette registered through add_theme_support, which added five swatches next to the picker rather than instead of it.
The symptom
$ grep -c 'add_theme_support' functions.php
14
$ wp post list --post_type=page --format=ids |
xargs -n1 wp post get --field=content |
grep -oE '#[0-9a-fA-F]{6}' | sort | uniq -c | sort -rn | head
41 #1f6feb
18 #1F6FEB
11 #1f6eeb ← a typo, on four pages
9 #2176f0
7 #1e6ce8
# five approximations of one brand colour, hard-coded
# into post content where no stylesheet can reach them.Colours written into the block markup cannot be changed by editing CSS, so a rebrand becomes a content migration. That is the cost of a picker being available, and no amount of documentation prevents it.
Why it happens
The theme and the editor had two independent descriptions of the design — a stylesheet and a set of add_theme_support calls — and neither was authoritative. Anything the theme did not declare, the editor offered from its own defaults.
The fix
The settings block, and what disabling means
{
"version": 1,
"settings": {
"color": {
"custom": false,
"customGradient": false,
"customDuotone": false,
"duotone": [],
"palette": [
{ "slug": "brand", "color": "#1f6feb", "name": "Brand" },
{ "slug": "ink", "color": "#101418", "name": "Ink" }
]
},
"typography": {
"customFontSize": false,
"dropCap": false,
"fontSizes": [
{ "slug": "large", "size": "1.5rem", "name": "Large" }
]
},
"spacing": { "units": ["px", "rem"], "padding": true }
}
}
"custom": false removes the control rather than changing a default, and that is the difference between a palette and a suggestion. The version field is required and is 1 in 5.8; version 2 arrives with 5.9 and renames several keys, so this file needs revisiting in six months.
The failure to plan for is existing content: a page that already has a custom colour keeps rendering it, because the value is in the block markup and only the control disappeared. Auditing for inline colour values is the companion job and there is no report for it — the grep above is the report.
The custom properties it generates
/* generated by WordPress, from the palette slugs */
:root {
--wp--preset--color--brand: #1f6feb;
--wp--preset--color--ink: #101418;
--wp--preset--font-size--small: 0.875rem;
--wp--custom--line-height--tight: 1.2;
}
/* so the stylesheet references the tokens rather than
duplicating the values */
.site-header { background: var(--wp--preset--color--ink); }
.callout { border-color: var(--wp--preset--color--brand); }
The naming is mechanical — --wp--preset--{category}--{slug}, with camelCase in the JSON becoming a hyphenated segment — which means one source of truth for a colour instead of two. The settings.custom section is the escape hatch for anything that is not a recognised preset category and is where a spacing scale belongs.
Duplicating a colour in both the JSON and the CSS is the exact mistake this exists to prevent, and it is easy to do while migrating because the stylesheet already has the hex value in it. A lint rule forbidding hex colours in the theme stylesheet is worth the ten minutes.
The styles block, which is the other half
{
"styles": {
"color": { "text": "var(--wp--preset--color--ink)" },
"typography": { "lineHeight": "1.6" },
"elements": {
"link": { "color": { "text": "var(--wp--preset--color--brand)" } }
},
"blocks": {
"core/quote": { "border": { "left": { "width": "3px" } } }
}
}
}
Styles declared here are emitted as CSS and are visible in the editor as well as on the front end, which is the whole reason the file exists — the editor no longer needs a separate editor stylesheet to look approximately right. In 5.8 the coverage is partial and a theme still needs its own stylesheet for anything structural.
The elements section is the one that is easy to miss and is the only way to style a link without a block selector. Per-block styles under blocks merge with the global ones rather than replacing them, which is usually what is wanted and occasionally is not.
Per-block settings, and when they help
{
"settings": {
"color": { "custom": false },
"blocks": {
"core/button": {
"color": { "palette": [
{ "slug": "cta", "color": "#c9372c", "name": "Call to action" }
] }
},
"core/separator": { "color": { "background": false } }
}
}
}
The useful cases are narrow: a colour that must appear on one block type and nowhere else, and disabling controls on a block where they produce nothing sensible. Using this heavily produces a file where the effective settings for any block require reading two places, which is how it stops being maintainable.
What 5.8 does not do
theme.json in 5.8 is NOT full site editing.
it does: the editor's settings, and global styles,
for a CLASSIC theme
it does not: block templates, template parts, the site
editor, or a theme built entirely of blocks
those are 5.9 in January. adopting theme.json now is a
step towards them and does not commit to them.Being clear about this internally mattered, because the announcements around 5.8 blurred the two and somebody will ask whether the theme is being rewritten. It is not — this is a settings file added to a classic theme, and it is the piece that will still be correct after the rewrite.
Verifying it worked
$ curl -s https://site.example | grep -A4 'wp-custom-properties'
:root{--wp--preset--color--brand: #1f6feb; ... }
$ grep -c 'add_theme_support' functions.php
3 # was 14 — the three left are unrelated
$ grep -oE '#[0-9a-fA-F]{6}' style.css | sort -u | wc -l
0
# and the visual check, which is the one that matters:
# the editor and the front end render the same colours,
# the same font sizes and the same line heightZero hex values in the theme stylesheet is the assertion that there is one source of truth, and it is worth a lint rule rather than a one-off check. The editor matching the front end is not testable by a command and is the outcome the whole file exists for.
The forty-one pages with inline colours still had them and had to be migrated separately — a WP-CLI script replacing known hex values with the corresponding preset slug, run against a copy first. Eleven of them had the typo and were quietly wrong before any of this started.
What this costs
A JSON file that is now the source of truth and a CSS file that will argue with it, because anything not expressible in theme.json still lives in the stylesheet and the boundary is not obvious. In 5.8 that boundary is generous — most structural styling is still CSS — and it moves with every release, which means this file needs revisiting rather than writing once.
Version 2 in 5.9 renames keys and adds nesting, so a file written in July needs a migration in January. That is a known cost of adopting something six months old and it is smaller than the cost of the eleven shades of blue.