Naming your theme.json colors after what they look like feels natural, but it falls apart fast. Here’s how I use semantic, role-based names, custom properties, and a few lessons learned to keep color manageable.
If you’ve built more than one block theme, you’ve probably hit the point where your theme.json color palette starts working against you. A client asks for a rebrand, or you want to add dark mode support, and suddenly you’re hunting through every template and block style trying to swap out light-gray for something else.
I’ve landed on an approach that makes this way less painful, and it comes down to how you name things.
Name Colors by Role, Not by Value
Here’s what a typical theme.json color palette looks like when you’re starting out:
{
"settings": {
"color": {
"palette": [
{ "slug": "white", "color": "#ffffff", "name": "White" },
{ "slug": "light-gray", "color": "#f0f0f0", "name": "Light Gray" },
{ "slug": "dark-blue", "color": "#1a2b4a", "name": "Dark Blue" },
{ "slug": "red", "color": "#c0392b", "name": "Red" }
]
}
}
}The names are technically correct, but they’re just named for the color they are. The biggest problem with naming colors like this is there is no obvious relationship between the colors of the palette.
For instance, how do you know if dark blue “goes with” light gray? Even if they’re an accessible color combination, it might not be on brand to use certain colors together. What if I wanted “something that looks good on top of dark blue”? The answer is far from obvious. You’d have to ask someone (such as the designer of the site) or just wing it.
That’s not a recipe for a consistent, polished design. Instead, I prefer name colors by what they do:
{
"settings": {
"color": {
"palette": [
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "base-2", "color": "#f5f7fa", "name": "Base 2" },
{ "slug": "base-3", "color": "#e8ecf1", "name": "Base 3" },
{ "slug": "contrast", "color": "#1a2b4a", "name": "Contrast" },
{ "slug": "contrast-2", "color": "#2e4163", "name": "Contrast 2" },
{ "slug": "contrast-3", "color": "#4a6080", "name": "Contrast 3" },
{ "slug": "primary", "color": "#3a7bd5", "name": "Primary" },
{ "slug": "accent", "color": "#c0392b", "name": "Accent" },
{ "slug": "subtle", "color": "#f0f0f0", "name": "Subtle" }
]
}
}
}Now when a client wants to change their brand color, you update one value in theme.json and everything that uses primary just follows along. Dark mode? Swap base and contrast. The slugs still make sense because they describe relationships, not specific hues.
I know that contrast is meant to go with base, and base-2 and contrast-2 go together, and so on. There isn’t one perfect way to name things, but using names that imply purpose makes much more sense than a simple description.
Handling Dark Mode
So once you have these colors set up, how would that help you do something like dark mode? The trick is to check the current user preference (either using prefers-color-scheme media query or some custom mechanism you’ve set up) and then set the generated CSS Variables from theme.json to a different value.
For instance, let’s say you wanted to swap the base/contrast colors for dark mode. You could accomplish that by using some styles like this:
html.dark-mode {
--wp--preset--color--base: #1a2b4a;
--wp--preset--color--base-2: #2e4163;
--wp--preset--color--base-3: #4a6080;
--wp--preset--color--contrast: #ffffff;
--wp--preset--color--contrast-2: #f5f7fa;
--wp--preset--color--contrast-3: #e8ecf1;
}
// Supports the user preference if they don't have light-mode explicitly set.
@media (prefers-color-scheme: dark) {
html:not(.light-mode) {
--wp--preset--color--base: #1a2b4a;
--wp--preset--color--base-2: #2e4163;
--wp--preset--color--base-3: #4a6080;
--wp--preset--color--contrast: #ffffff;
--wp--preset--color--contrast-2: #f5f7fa;
--wp--preset--color--contrast-3: #e8ecf1;
}
}The other week I wrote an article that shows you how to create a light/dark mode toggle block using the WP Interactivity API. Check it out for more details on setting up a dark/light mode toggle.
Custom Properties Outside the Palette
The palette in theme.json is great for colors that show up in the editor’s color picker. But sometimes you need colors that are more of an internal system, things like border shades, subtle hover states, or a Tailwind-style numeric scale for fine-grained control.
You can define those as custom properties in theme.json without adding them to the palette:
{
"settings": {
"custom": {
"color": {
"surface": {
"100": "#f8f9fa",
"200": "#e9ecef",
"300": "#dee2e6",
"400": "#ced4da"
},
"border": {
"light": "#e0e0e0",
"default": "#cccccc"
}
}
}
}
}These values won’t clutter up the color picker in the editor, which keeps the editing experience clean for your client. But they’re still available to use in your CSS.
When you add a key under custom you can give it any camelCase name you want and it will create a hyphenated version to use as part of the variable name. When you nest values like in the above example (settings.custom.color.surface.100) each “level” will be used to build the name. This generates CSS custom properties like --wp--custom--color--surface--100 that you can use in your theme’s CSS.
The settings.custom section of theme.json is a veritable CSS Variable generator. What’s more, these variable definitions will be available in the block editor, site editor, and frontend automatically. You won’t have to worry about including a separate stylesheet or any of that kinda jazz.
A Note on Raw Values vs. CSS Variables
One thing I’ve bumped into: theme.json works better when you define your palette colors as raw hex (or rgb) values rather than referencing CSS variables in the color definitions themselves.
You might be tempted to do something like this, defining a set of CSS variables and then referencing them in your palette:
{
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "var(--brand-primary)", "name": "Primary" }
]
}
}
}WordPress will try to look at the value of something like –wp–preset–color–primary to determine if its color has the right contrast compared to the other colors in use by a given block. If the value of such a preset is a CSS variable, things like those a11y1 checks will not work as expected.
Additionally, you have to do extra work to make sure the value of something like --brand-primary is set where it needs to be.
Keep raw values in your palette definitions and use the generated --wp--preset--color--primary variable everywhere else in your CSS. Let theme.json be the source of truth for the actual values.
The Payoff
This approach is pretty simple once you get used to it, but it saves a ton of time when things change (and they always do). But most importantly, if you’re building a reusable system (such as a starter theme you’re using on projects) this makes it way easier to build premade components. You can just plug in the colors and you’re off and running.
With this approach, things like dark mode become a realistic option instead of a nightmare. And the numbered custom property system gives you the flexibility to handle edge cases without polluting the editor UI.
It’s a small shift in how you think about naming, but it makes your theme way more resilient. This is a pattern used by complex enterprise projects that you can use for your benefit in a project of any size.
Happy coding!
- Accessibility is often shortened to “a11y” because there are 11 characters between the “a” and “y” in the word accessibility. ↩︎