Every value you define in theme.json becomes a CSS custom property. Colors, font sizes, spacing scales, WordPress converts them all into --wp--preset-- variables that cascade through every block on the page.

Understanding this system is the key to scalable block theme styling. Instead of writing override rules for individual blocks, you define your design tokens once in theme.json and reference them everywhere through CSS variables. Change a color in one place, and it updates across your entire theme.

This guide covers how WordPress generates CSS custom properties from theme.json, how to use them in your own CSS, how to override core block variables, and how to create a naming convention that scales.

How theme.json Generates CSS Custom Properties

When WordPress processes your theme.json file, it converts every preset value into a CSS custom property following a predictable naming pattern:

--wp--preset--{type}--{slug}

Here’s what that looks like in practice. Define a color palette in theme.json:

{
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary", "color": "#1e3a5f", "name": "Primary" },
        { "slug": "secondary", "color": "#4a90d9", "name": "Secondary" },
        { "slug": "accent", "color": "#e8491d", "name": "Accent" },
        { "slug": "light", "color": "#f5f5f5", "name": "Light" },
        { "slug": "dark", "color": "#1a1a1a", "name": "Dark" }
      ]
    }
  }
}

WordPress generates these CSS custom properties on the body element:

body {
  --wp--preset--color--primary: #1e3a5f;
  --wp--preset--color--secondary: #4a90d9;
  --wp--preset--color--accent: #e8491d;
  --wp--preset--color--light: #f5f5f5;
  --wp--preset--color--dark: #1a1a1a;
}

The same pattern applies to every preset type:

theme.json Setting CSS Custom Property Pattern Example
color.palette --wp--preset--color--{slug} --wp--preset--color--primary
color.gradients --wp--preset--gradient--{slug} --wp--preset--gradient--vivid-cyan
typography.fontSizes --wp--preset--font-size--{slug} --wp--preset--font-size--large
typography.fontFamilies --wp--preset--font-family--{slug} --wp--preset--font-family--heading
spacing.spacingSizes --wp--preset--spacing--{slug} --wp--preset--spacing--50
shadow.presets --wp--preset--shadow--{slug} --wp--preset--shadow--natural

Using Preset CSS Variables in theme.json

You can reference these variables within theme.json itself using the var:preset|type|slug syntax. This is how you connect settings to styles without hardcoding values.

{
  "styles": {
    "color": {
      "background": "var:preset|color|light",
      "text": "var:preset|color|dark"
    },
    "elements": {
      "link": {
        "color": {
          "text": "var:preset|color|primary"
        },
        ":hover": {
          "color": {
            "text": "var:preset|color|accent"
          }
        }
      },
      "heading": {
        "color": {
          "text": "var:preset|color|primary"
        },
        "typography": {
          "fontFamily": "var:preset|font-family|heading"
        }
      }
    }
  }
}

WordPress converts var:preset|color|primary to var(--wp--preset--color--primary) in the generated CSS. This means your entire global styles configuration references the same set of design tokens.

Custom CSS Properties Beyond Presets

WordPress presets cover colors, fonts, and spacing. But what about border radii, transition durations, z-index values, or component-specific tokens? That’s where the custom section in theme.json comes in.

{
  "settings": {
    "custom": {
      "borderRadius": {
        "small": "4px",
        "medium": "8px",
        "large": "16px",
        "pill": "9999px"
      },
      "transition": {
        "duration": "200ms",
        "easing": "ease-in-out"
      },
      "lineHeight": {
        "tight": "1.2",
        "normal": "1.6",
        "loose": "2.0"
      },
      "contentWidth": "720px",
      "wideWidth": "1200px"
    }
  }
}

WordPress generates these as --wp--custom-- properties, converting camelCase to kebab-case and nesting with double dashes:

body {
  --wp--custom--border-radius--small: 4px;
  --wp--custom--border-radius--medium: 8px;
  --wp--custom--border-radius--large: 16px;
  --wp--custom--border-radius--pill: 9999px;
  --wp--custom--transition--duration: 200ms;
  --wp--custom--transition--easing: ease-in-out;
  --wp--custom--line-height--tight: 1.2;
  --wp--custom--line-height--normal: 1.6;
  --wp--custom--line-height--loose: 2.0;
  --wp--custom--content-width: 720px;
  --wp--custom--wide-width: 1200px;
}

Now use these in your theme’s stylesheet:

/* style.css */
.wp-block-button .wp-block-button__link {
  border-radius: var(--wp--custom--border-radius--medium);
  transition: all var(--wp--custom--transition--duration) var(--wp--custom--transition--easing);
}

.wp-block-quote {
  border-left: 4px solid var(--wp--preset--color--accent);
  border-radius: var(--wp--custom--border-radius--small);
}

.wp-block-separator {
  max-width: var(--wp--custom--content-width);
}

Overriding Core Block CSS Variables

WordPress core blocks use CSS custom properties internally. Knowing which variables a block uses lets you restyle it without fighting specificity wars.

Common Block Variables

/* Button block */
.wp-block-button .wp-block-button__link {
  /* These come from theme.json styles.blocks.core/button */
  background-color: var(--wp--preset--color--primary);
  color: var(--wp--preset--color--light);
  font-size: var(--wp--preset--font-size--medium);
}

/* Cover block */
.wp-block-cover {
  /* Gap between inner elements */
  gap: var(--wp--style--block-gap);
  /* Content width */
  --wp--style--global--content-size: var(--wp--custom--content-width);
  --wp--style--global--wide-size: var(--wp--custom--wide-width);
}

/* Group block */
.wp-block-group {
  /* Padding uses spacing presets */
  padding: var(--wp--preset--spacing--50);
}

Overriding via theme.json (Preferred)

The cleanest way to override block styles is through the styles.blocks section in theme.json:

{
  "styles": {
    "blocks": {
      "core/button": {
        "border": {
          "radius": "var:custom|border-radius|medium"
        },
        "typography": {
          "fontWeight": "600",
          "letterSpacing": "0.02em"
        },
        "variations": {
          "outline": {
            "border": {
              "width": "2px"
            }
          }
        }
      },
      "core/quote": {
        "border": {
          "left": {
            "color": "var:preset|color|accent",
            "width": "4px",
            "style": "solid"
          }
        },
        "spacing": {
          "padding": {
            "left": "var:preset|spacing|40"
          }
        }
      },
      "core/code": {
        "color": {
          "background": "var:preset|color|dark",
          "text": "var:preset|color|light"
        },
        "border": {
          "radius": "var:custom|border-radius|small"
        },
        "typography": {
          "fontFamily": "var:preset|font-family|monospace",
          "fontSize": "var:preset|font-size|small"
        }
      }
    }
  }
}

This generates CSS with the correct specificity, no !important hacks needed.

Overriding via CSS (When Needed)

For overrides that theme.json can’t express (pseudo-elements, complex selectors, animations), use your theme’s style.css:

/* Custom focus styles for buttons */
.wp-block-button .wp-block-button__link:focus-visible {
  outline: 2px solid var(--wp--preset--color--accent);
  outline-offset: 2px;
}

/* Custom list markers */
.wp-block-list li::marker {
  color: var(--wp--preset--color--accent);
}

/* Code block with line numbers effect */
.wp-block-code code {
  line-height: var(--wp--custom--line-height--loose);
  tab-size: 2;
}

Creating a Variable Naming Convention

As your theme grows, custom properties can become chaotic. A naming convention prevents this. Here’s a system that scales:

Three-Tier Token System

{
  "settings": {
    "custom": {
      "color": {
        "brand": {
          "primary": "#1e3a5f",
          "primaryLight": "#2d5a8e",
          "primaryDark": "#0f1d30"
        },
        "surface": {
          "default": "#ffffff",
          "muted": "#f5f5f5",
          "inverse": "#1a1a1a"
        },
        "text": {
          "default": "#333333",
          "muted": "#666666",
          "inverse": "#ffffff",
          "link": "#1e3a5f",
          "linkHover": "#e8491d"
        },
        "feedback": {
          "success": "#28a745",
          "warning": "#ffc107",
          "error": "#dc3545",
          "info": "#17a2b8"
        }
      },
      "spacing": {
        "component": {
          "paddingXs": "0.5rem",
          "paddingSm": "0.75rem",
          "paddingMd": "1rem",
          "paddingLg": "1.5rem"
        },
        "section": {
          "sm": "2rem",
          "md": "4rem",
          "lg": "6rem",
          "xl": "8rem"
        }
      }
    }
  }
}

This generates organized variables:

--wp--custom--color--brand--primary
--wp--custom--color--surface--default
--wp--custom--color--text--muted
--wp--custom--color--feedback--error
--wp--custom--spacing--component--padding-md
--wp--custom--spacing--section--lg

The naming follows a clear hierarchy: category → role → variant. When a developer sees --wp--custom--color--text--muted, they immediately know it’s a muted text color, no guessing required.

Naming Rules

  • Use semantic names, primary, surface, feedback tell you purpose. blue-500 tells you nothing about where to use it.
  • Group by function, color/brand, color/surface, color/text, color/feedback. Not color/blue, color/gray, color/red.
  • Keep slugs short, sm, md, lg for sizes. xs through xl for scales. Shorter slugs mean shorter generated variable names.
  • Don’t duplicate presets, If a value is in your color palette or font size presets, don’t also put it in custom. Use var:preset|color|primary to reference it.

Bridging theme.json and Custom CSS

The real power comes from using theme.json variables in your custom CSS. This creates a single source of truth, all styling decisions flow from theme.json.

Pattern 1: Component Styles

/* Card component using theme tokens */
.wp-block-group.is-style-card {
  background: var(--wp--custom--color--surface--default);
  border: 1px solid var(--wp--custom--color--surface--muted);
  border-radius: var(--wp--custom--border-radius--medium);
  padding: var(--wp--custom--spacing--component--padding-lg);
  transition: box-shadow var(--wp--custom--transition--duration) var(--wp--custom--transition--easing);
}

.wp-block-group.is-style-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

Pattern 2: Responsive Spacing

/* Section spacing that adapts to viewport */
.wp-block-group.alignfull {
  padding-block: var(--wp--custom--spacing--section--md);
}

@media (min-width: 768px) {
  .wp-block-group.alignfull {
    padding-block: var(--wp--custom--spacing--section--lg);
  }
}

@media (min-width: 1200px) {
  .wp-block-group.alignfull {
    padding-block: var(--wp--custom--spacing--section--xl);
  }
}

Pattern 3: Dark Mode with CSS Variables

CSS custom properties make dark mode straightforward. Override your surface and text variables at the component level:

/* Dark section override */
.wp-block-group.has-dark-background-color {
  --wp--custom--color--surface--default: var(--wp--preset--color--dark);
  --wp--custom--color--text--default: var(--wp--preset--color--light);
  --wp--custom--color--text--muted: #aaaaaa;
}

/* All child elements automatically inherit the overridden values */
.wp-block-group.has-dark-background-color .wp-block-heading {
  color: var(--wp--custom--color--text--default);
  /* This now resolves to light color, not dark */
}

This approach works because CSS custom properties inherit. Set them on a parent, and every child that references those variables gets the updated value. No extra selectors needed.

Style Variations and CSS Properties

Block theme style variations override theme.json values, which means they override the CSS custom properties too.

Create a style variation file at styles/dark.json:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "title": "Dark",
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary", "color": "#4a90d9", "name": "Primary" },
        { "slug": "light", "color": "#1a1a1a", "name": "Light" },
        { "slug": "dark", "color": "#f5f5f5", "name": "Dark" }
      ]
    },
    "custom": {
      "color": {
        "surface": {
          "default": "#1a1a1a",
          "muted": "#2d2d2d"
        },
        "text": {
          "default": "#e0e0e0",
          "muted": "#999999"
        }
      }
    }
  }
}

When a user selects the Dark variation in the Site Editor, WordPress regenerates all CSS custom properties with the new values. Every component using those variables updates automatically. Zero additional CSS required.

Debugging CSS Custom Properties

When variables don’t resolve as expected:

  • Browser DevTools, Inspect the body element to see all generated --wp--preset-- and --wp--custom-- variables. Check that your expected variables exist.
  • Check the slug, theme.json slugs convert camelCase to kebab-case. "borderRadius" becomes --wp--custom--border-radius. A typo in the slug means the variable silently doesn’t exist.
  • Specificity issues, If a theme.json style sets a value and your CSS tries to override it, you might need higher specificity. Use .wp-site-blocks as a parent selector to increase specificity without !important.
  • Caching, WordPress caches the generated CSS from theme.json. After changes, clear any full-page or object caches to see updated variables.
  • Fallback values, Always provide fallbacks for custom properties: color: var(--wp--custom--color--text--default, #333). If the variable doesn’t resolve, the fallback prevents invisible text or broken layouts.

Performance Considerations

CSS custom properties have minimal performance impact, but there are patterns to follow:

  • Define at the highest level, Variables defined in theme.json appear on body, which is optimal. Avoid redefining the same variable on hundreds of elements.
  • Limit nesting depth, --wp--custom--spacing--component--padding-md is fine. Going deeper (5+ levels) makes the variable name unwieldy without adding organizational value.
  • Use presets when possible, WordPress presets are optimized. Duplicating preset values in custom creates redundant CSS output.
  • Avoid unused variables, Each custom property in theme.json adds to the generated CSS. Remove variables you’ve stopped using.

The modern CSS features available in 2026 (nesting, container queries, :has()) combine well with custom properties. Use CSS nesting to scope variable overrides to specific contexts without increasing selector complexity.

Frequently Asked Questions

Can I use CSS custom properties that aren’t defined in theme.json?

Yes. You can define standard CSS custom properties in your style.css with :root { --my-var: value; }. But these won’t appear in the Site Editor’s Global Styles UI. For anything you want users to be able to change through the editor, define it in theme.json.

Do custom properties work with the block editor’s color picker?

Only preset colors (color.palette) appear in the editor’s color picker. Custom properties defined in the custom section are available in CSS but don’t show in the visual editor controls.

How do I override a core WordPress preset variable?

Define a preset with the same slug in your theme.json. Your theme’s palette overrides core’s default palette. For example, if you define a color with slug "vivid-cyan-blue", it replaces WordPress core’s default.

What happens to custom properties when a plugin adds its own?

Plugins can add their own presets through theme.json in their plugin directory. These merge with your theme’s values. If slugs conflict, the theme’s values take priority in most cases. Use unique slug prefixes to avoid conflicts.

Are CSS custom properties supported in all browsers?

Yes. CSS custom properties have 97%+ browser support as of 2026, including all modern versions of Chrome, Firefox, Safari, and Edge. IE11 is the only notable exception, and WordPress dropped IE11 support in WordPress 5.8.

What to Build Next

Start with your theme.json color palette and typography presets, these are the variables you’ll reference most often. Add custom properties for border radii, transitions, and spacing scales as you need them.

Then create block patterns that use these variables. A card pattern, a hero section, a testimonial, each referencing your design tokens instead of hardcoded values. When you update theme.json, every pattern updates with it.

The goal is a theme where every visual decision traces back to theme.json. Custom CSS handles the layout and interactivity that theme.json can’t express, but all values come from variables. That’s scalable styling.