Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 027 / 100

theme.json explained: WordPress best practices explained

Global styles and settings.

Expona Admin6 min read

Theme.json explained: WordPress best practices explained

If you're building WordPress themes in 2024, you're likely encountering theme.json at some point—whether you've adopted the Full Site Editing (FSE) model or you're supporting both classic and block-based workflows. theme.json is the declarative configuration file that replaces scattered theme supports, custom CSS, and editor restrictions with a single source of truth. Once you understand how to leverage it properly, you'll spend less time writing boilerplate code and more time building features that matter to your clients.

What is theme.json and why it matters

theme.json is a JSON configuration file that lives in your theme root (or child theme root). It centralizes styling, spacing, typography, color, and editor capabilities in a structured, machine-readable format. Instead of using PHP filters like add_theme_support() and inline stylesheets, you declare what your theme supports, what settings users can modify, and how blocks should behave globally.

For teams managing multiple sites or working with non-technical editors, this is transformative. Editors get a consistent experience across all blocks, you enforce brand guidelines without custom code on every page, and you reduce the surface area for CSS conflicts.

Define color palette and fonts

One of the most powerful features of theme.json is centralized color and typography control. Rather than hardcoding hex values or font stacks throughout your CSS, you define them once and WordPress automatically generates CSS custom properties that blocks can reference.

Here's a practical color palette setup:

php
{
  "version": 2,
  "settings": {
    "color": {
      "palette": [
        {
          "color": "#000000",
          "name": "Black",
          "slug": "black"
        },
        {
          "color": "#ffffff",
          "name": "White",
          "slug": "white"
        },
        {
          "color": "#0073aa",
          "name": "Primary",
          "slug": "primary"
        },
        {
          "color": "#f0ad4e",
          "name": "Warning",
          "slug": "warning"
        }
      ]
    }
  }
}

WordPress transforms this into CSS custom properties like var(--wp--preset--color--primary), which appear automatically in the color picker for every block. This means your editor never has to remember the hex value for your brand blue—they select "Primary" and get consistent results site-wide.

For fonts, you define a font family palette the same way:

php
{
  "version": 2,
  "settings": {
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
          "name": "System",
          "slug": "system"
        },
        {
          "fontFamily": "'Georgia', 'Garamond', serif",
          "name": "Serif",
          "slug": "serif"
        }
      ]
    }
  }
}

Combined with fontSizes, you can enforce a typographic scale. Rather than letting editors pick arbitrary sizes, you offer "Small" (0.875rem), "Base" (1rem), "Large" (1.25rem), and "Heading" (2rem). This ensures better hierarchy and readability across your site.

Control which settings editors can change

A common pain point: you set up a beautiful color scheme, then an editor adds a random custom color and breaks your design. theme.json gives you granular control over what's editable.

Use the "settings" object to enable or disable capabilities per block, or globally:

php
{
  "version": 2,
  "settings": {
    "color": {
      "custom": false,
      "customDuelist": false
    },
    "typography": {
      "customFontSize": false,
      "customLineHeight": false
    },
    "spacing": {
      "custom": false,
      "padding": true,
      "margin": true
    }
  },
  "blocks": {
    "core/heading": {
      "settings": {
        "color": {
          "custom": false
        }
      }
    }
  }
}

In this configuration, editors can only use predefined colors (no custom color picker), can't adjust font sizes beyond your scale, and can't set arbitrary spacing values. But they can still adjust padding and margins on individual elements using your spacing scale.

The "blocks" key lets you be even more specific. If you want heading blocks to be strictly on-brand with no color override, set "custom": false just for core/heading. Meanwhile, paragraph blocks get full flexibility.

Per-block styles and defaults

Beyond global settings, you can define default styles and capabilities for specific blocks. This is where you lock down behavior without touching custom post meta or using filters.

Let's say you want all buttons to have rounded corners, your brand primary color, and a minimum size:

php
{
  "version": 2,
  "styles": {
    "blocks": {
      "core/button": {
        "color": {
          "background": "var(--wp--preset--color--primary)",
          "text": "var(--wp--preset--color--white)"
        },
        "border": {
          "radius": "0.5rem"
        },
        "spacing": {
          "padding": {
            "top": "0.75rem",
            "right": "1.5rem",
            "bottom": "0.75rem",
            "left": "1.5rem"
          }
        }
      }
    }
  }
}

Every new button block inserted on the site inherits these defaults. Editors can still override individual buttons if needed (depending on your permission settings), but the baseline is locked in.

You can also nest variations for specific button styles. If you've defined button variations in your block.json, you can style each variant independently here:

php
{
  "blocks": {
    "core/button": {
      "variations": {
        "outline": {
          "color": {
            "background": "transparent",
            "text": "var(--wp--preset--color--primary)"
          },
          "border": {
            "style": "solid",
            "width": "2px",
            "color": "var(--wp--preset--color--primary)"
          }
        }
      }
    }
  }
}

Generates CSS custom properties automatically

Behind the scenes, WordPress parses your theme.json and generates a stylesheet with hundreds of CSS custom properties. You never write it manually—WordPress builds it automatically.

If you've defined a color slug "primary", WordPress outputs:

php
--wp--preset--color--primary: #0073aa;

You can reference this anywhere in your custom CSS, block templates, or inline styles:

php
.custom-box {
  border-left: 4px solid var(--wp--preset--color--primary);
  background-color: var(--wp--preset--color--light-gray);
}

The same applies to typography, spacing, and other presets. This means your stylesheets stay lean, updates propagate instantly (change the primary color in theme.json, it updates everywhere), and you have a single source of truth for your design system.

WordPress also exposes these custom properties in the block editor, so when an editor uses the color picker, they're selecting from the same palette your CSS is using. No mismatch between frontend and editor.

Common mistakes

Overcomplicating the structure. Don't nest settings ten levels deep to cover edge cases you haven't hit yet. Start simple—define your colors, fonts, and core block styles. Add complexity only when a real constraint emerges.

Forgetting version compatibility. Always include "version": 2 at the root. Older versions of WordPress used version 1, and the schema differs. If you're targeting WordPress 6.0+, use version 2.

Not testing editor behavior. Disabling "custom": true feels good for consistency, but test with real editors first. If they can't adjust margins on a specific page without hacking around your restrictions, you've gone too far. Balance enforcement with usability.

Ignoring responsive settings. theme.json supports responsive presets. If you define spacing or typography scales but ignore mobile breakpoints, your site may look cramped on small screens. Use "mobile", "tablet", and "desktop" keys when needed.

Mixing theme.json with legacy PHP theme supports. If you use both theme.json and add_theme_support( 'custom-line-height' ), behavior can be unpredictable. Pick one approach per setting and stick with it. For new projects, lean entirely on theme.json.

Key takeaways

  • theme.json is your single source of truth for design system enforcement, replacing scattered theme supports and CSS overrides.
  • Define color palettes and font families once; WordPress auto-generates CSS custom properties that blocks reference automatically.
  • Use the "settings" object globally and per-block to restrict what editors can customize—disable custom colors or sizes to keep designs on-brand.
  • Style individual blocks and their variations in "styles.blocks" to set sensible defaults without forcing overrides.
  • Test editor workflows before locking down constraints; usability matters as much as consistency.
  • Stick with version: 2 for modern WordPress themes and avoid mixing theme.json with legacy PHP approaches.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles