CSS custom properties guide for WordPress theme development

CSS custom properties — also called CSS variables — are the foundation of modern WordPress block theme design systems. They allow you to define a value once (a colour, a font size, a spacing unit) and reference it throughout your stylesheet, so changing the brand colour requires editing one line rather than hundreds. Since WordPress 5.8, theme.json automatically generates CSS custom properties from the palette, typography, and spacing settings you define, using a predictable naming convention like --wp--preset--color--primary and --wp--preset--font-size--large. This means the block editor, the block settings panel, and your theme’s style.css all reference the same token, keeping design and code perfectly in sync. Beyond the built-in WordPress tokens, you can define your own custom properties on the :root selector for spacing scales, shadow definitions, border radii, transition durations, and any other design decision that should be configurable. The var() function references a custom property and accepts a fallback value as a second argument — var(--ha-primary, #2563eb) uses #2563eb if the variable is not defined. Custom properties are inherited through the DOM, which makes them powerful for component-level theming: setting --ha-card-bg on a parent element automatically applies to all child card components. They can also be updated at runtime with JavaScript using document.documentElement.style.setProperty(), enabling dynamic theming without page reload. Review the CSS Grid guide and the clip-path guide for layout techniques that pair naturally with a custom property design system.

Problem: Colours, spacing values, and font sizes are hardcoded throughout the theme stylesheet, making global design changes tedious and keeping the theme out of sync with the block editor settings.

Solution: Define a design token layer with CSS custom properties and reference the WordPress-generated tokens from theme.json:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 2,
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary",   "color": "#2563eb", "name": "Primary"   },
        { "slug": "secondary", "color": "#7c3aed", "name": "Secondary" },
        { "slug": "neutral",   "color": "#6b7280", "name": "Neutral"   },
        { "slug": "surface",   "color": "#f9fafb", "name": "Surface"   }
      ]
    },
    "typography": {
      "fontSizes": [
        { "slug": "sm",   "size": "0.875rem", "name": "Small"       },
        { "slug": "base", "size": "1rem",     "name": "Base"        },
        { "slug": "lg",   "size": "1.25rem",  "name": "Large"       },
        { "slug": "xl",   "size": "1.5rem",   "name": "Extra Large" },
        { "slug": "2xl",  "size": "2rem",     "name": "2XL"         }
      ]
    },
    "spacing": {
      "spacingSizes": [
        { "slug": "4",  "size": "1rem",  "name": "4"  },
        { "slug": "8",  "size": "2rem",  "name": "8"  },
        { "slug": "16", "size": "4rem",  "name": "16" }
      ]
    }
  }
}

/* style.css — reference WordPress-generated tokens + add custom ones */

:root {
    /* Custom tokens that extend theme.json */
    --ha-shadow-sm:     0 1px 2px 0 rgb(0 0 0 / 0.05);
    --ha-shadow-md:     0 4px 6px -1px rgb(0 0 0 / 0.1);
    --ha-radius-sm:     0.25rem;
    --ha-radius-md:     0.5rem;
    --ha-radius-lg:     1rem;
    --ha-transition:    0.2s ease;
    --ha-content-width: 65ch;
}

/* Use WP-generated tokens from theme.json */
.site-header {
    background-color: var(--wp--preset--color--primary);
    padding: var(--wp--preset--spacing--8);
}

/* Card component using custom tokens */
.card {
    background:    var(--wp--preset--color--surface);
    border-radius: var(--ha-radius-md);
    box-shadow:    var(--ha-shadow-md);
    padding:       var(--wp--preset--spacing--8);
    transition:    box-shadow var(--ha-transition);
}
.card:hover {
    box-shadow: var(--ha-shadow-sm);
}

/* Dark mode via custom property override */
@media (prefers-color-scheme: dark) {
    :root {
        --wp--preset--color--surface: #1f2937;
        --ha-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
    }
}

/* Runtime theme switch via JavaScript */
/* document.documentElement.style.setProperty('--wp--preset--color--primary', '#dc2626'); */

NOTE: WordPress generates custom property names from theme.json slugs using the pattern --wp--preset--{category}--{slug}. Use lowercase, hyphen-separated slugs in theme.json to match this pattern exactly. For spacing, WordPress generates --wp--preset--spacing--{slug} only when settings.spacing.spacingScale or settings.spacing.spacingSizes is defined — enabling settings.spacing.units alone is not enough. Validate that your tokens are generated correctly by inspecting the inline <style> tag in the page <head> that WordPress outputs for theme.json styles.