Tutorial 2026-03-04 By Marcus Chen

WordPress theme.json Explained: The Complete Configuration Guide

Everything you need to know about theme.json, the single file that controls colors, typography, spacing, and layout in WordPress block themes.

When I first opened a theme.json file, I stared at it for a solid ten minutes. It looked like a config file that was trying to do too many things at once. Colors, fonts, spacing, layout widths, block-level overrides, custom templates... all in one JSON file.

But once I understood the structure, it clicked. This single file replaces dozens of add_theme_support() calls, custom CSS files, and Customizer settings that classic themes needed. If you're building or customizing a block theme, theme.json is the control center for everything visual.

What Is theme.json?

In classic WordPress themes, you'd register features like custom colors, font sizes, and layout widths in functions.php:

// The old way (classic themes)
add_theme_support( 'editor-color-palette', array(
    array( 'name' => 'Primary', 'slug' => 'primary', 'color' => '#0073aa' ),
));
add_theme_support( 'responsive-embeds' );
add_theme_support( 'custom-spacing' );

Block themes replace all of that with theme.json. One file, JSON format, no PHP required. WordPress reads it during page load and generates the CSS automatically.

This matters for two reasons. First, theme developers write less boilerplate code. Second, users get a consistent experience in the Site Editor because the same settings that control the front end also control what appears in the editing interface.

The File Structure

Every theme.json file follows the same top-level structure:

{
  "$schema": "https://schemas.wp.org/wp/6.7/theme.json",
  "version": 3,
  "settings": { },
  "styles": { },
  "customTemplates": [ ],
  "templateParts": [ ]
}

Here's what each section does:

  • $schema: Points to WordPress's JSON schema file. Your code editor uses this for autocomplete and validation. Update the version number to match your WordPress version.
  • version: The theme.json API version. Use 3 for WordPress 6.6+. Older sites might need version 2.
  • settings: Defines what options are available (color palettes, font choices, spacing units).
  • styles: Applies default values to those options (the actual colors, font sizes, margins).
  • customTemplates: Registers custom page templates that show up in the editor.
  • templateParts: Defines reusable sections like headers and footers.

The mental model is simple: settings defines what's possible, styles defines what's default.

Settings Section

This is where you define your design toolkit. Everything in the settings section controls what appears in the Site Editor's style panels.

Color Palettes

"settings": {
  "color": {
    "palette": [
      { "slug": "primary", "color": "#1a5276", "name": "Primary" },
      { "slug": "secondary", "color": "#2ecc71", "name": "Secondary" },
      { "slug": "dark", "color": "#1c1c1c", "name": "Dark" },
      { "slug": "light", "color": "#fafafa", "name": "Light" }
    ],
    "gradients": [
      {
        "slug": "primary-to-secondary",
        "gradient": "linear-gradient(135deg, #1a5276 0%, #2ecc71 100%)",
        "name": "Primary to Secondary"
      }
    ],
    "custom": false,
    "defaultPalette": false
  }
}

Setting "custom": false disables the color picker, so editors can only choose from your defined palette. Set "defaultPalette": false to hide WordPress's built-in colors. This is how you keep brand consistency across a site with multiple editors.

Typography

"typography": {
  "fontFamilies": [
    {
      "fontFamily": "\"Inter\", sans-serif",
      "slug": "body",
      "name": "Inter",
      "fontFace": [
        {
          "fontFamily": "Inter",
          "fontWeight": "400 700",
          "fontStyle": "normal",
          "src": ["file:./assets/fonts/inter-variable.woff2"]
        }
      ]
    }
  ],
  "fontSizes": [
    { "slug": "small", "size": "0.875rem", "name": "Small" },
    { "slug": "medium", "size": "1rem", "name": "Medium" },
    { "slug": "large", "size": "1.25rem", "name": "Large" },
    { "slug": "x-large", "size": "1.75rem", "name": "Extra Large" }
  ],
  "lineHeight": true,
  "letterSpacing": true,
  "customFontSize": false
}

The fontFace property lets you bundle local font files with your theme, which is better for performance and privacy than loading from Google Fonts. Put your WOFF2 files in assets/fonts/ inside your theme folder.

Spacing and Layout

"spacing": {
  "units": ["px", "rem", "%", "vw"],
  "padding": true,
  "margin": true,
  "blockGap": true
},
"layout": {
  "contentSize": "720px",
  "wideSize": "1100px"
}

contentSize sets the default width for content blocks, while wideSize sets the width for blocks that use the "wide" alignment. These values show up when editing any block with alignment controls.

Per-Block Settings

You can restrict or expand settings for specific blocks:

"blocks": {
  "core/paragraph": {
    "color": {
      "custom": false
    }
  },
  "core/heading": {
    "typography": {
      "fontSizes": [
        { "slug": "heading-sm", "size": "1.5rem", "name": "Heading Small" },
        { "slug": "heading-lg", "size": "2.5rem", "name": "Heading Large" }
      ]
    }
  }
}

This removes the custom color picker from paragraphs while giving headings their own set of font sizes. Useful when you want to give editors flexibility in some areas but lock down others.

Styles Section

While settings define the options, styles set the defaults. Think of it as the CSS your theme ships with, but written in JSON instead.

Global Styles

"styles": {
  "color": {
    "background": "#ffffff",
    "text": "#1c1c1c"
  },
  "typography": {
    "fontFamily": "var(--wp--preset--font-family--body)",
    "fontSize": "1rem",
    "lineHeight": "1.7"
  },
  "spacing": {
    "padding": {
      "top": "0",
      "right": "1.5rem",
      "bottom": "0",
      "left": "1.5rem"
    }
  }
}

Notice the var(--wp--preset--font-family--body) syntax. WordPress generates CSS custom properties from your settings automatically. The naming convention is --wp--preset--{type}--{slug}.

Element Styles

Target HTML elements directly:

"elements": {
  "heading": {
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--heading)",
      "fontWeight": "700"
    },
    "color": {
      "text": "#1a5276"
    }
  },
  "link": {
    "color": {
      "text": "#1a5276"
    },
    ":hover": {
      "color": {
        "text": "#2ecc71"
      }
    }
  }
}

Yes, you can define hover states in theme.json. WordPress added pseudo-class support in version 6.1. It works for :hover, :focus, and :active on links and buttons.

Block-Level Styles

"blocks": {
  "core/button": {
    "border": {
      "radius": "6px"
    },
    "typography": {
      "fontWeight": "600",
      "fontSize": "0.95rem"
    },
    "color": {
      "background": "var(--wp--preset--color--primary)",
      "text": "#ffffff"
    }
  },
  "core/quote": {
    "border": {
      "left": {
        "color": "var(--wp--preset--color--primary)",
        "width": "4px",
        "style": "solid"
      }
    }
  }
}

This gives all buttons a 6px border radius and custom colors, and all quotes a left border accent. Users can still override these per-block in the editor, but these become the defaults.

Custom Templates and Template Parts

Register custom page templates that editors can select from the page settings panel:

"customTemplates": [
  {
    "name": "page-no-title",
    "title": "Page (No Title)",
    "postTypes": ["page"]
  },
  {
    "name": "page-full-width",
    "title": "Full Width",
    "postTypes": ["page", "post"]
  }
]

Each entry needs a matching HTML file in your theme's templates/ folder (e.g., templates/page-no-title.html). For template parts like headers and footers:

"templateParts": [
  { "name": "header", "title": "Header", "area": "header" },
  { "name": "footer", "title": "Footer", "area": "footer" },
  { "name": "sidebar", "title": "Sidebar", "area": "uncategorized" }
]

Template parts live in the parts/ folder. The area property tells the Site Editor where this part belongs, which helps it display the right editing UI. For a deeper look at how FSE (Full Site Editing) puts all these pieces together, see our guide to mastering WordPress FSE.

Practical Examples

Dark Mode Palette

Here's a complete dark mode setup I used for a developer portfolio site:

{
  "$schema": "https://schemas.wp.org/wp/6.7/theme.json",
  "version": 3,
  "settings": {
    "color": {
      "palette": [
        { "slug": "base", "color": "#0d1117", "name": "Base" },
        { "slug": "surface", "color": "#161b22", "name": "Surface" },
        { "slug": "text-primary", "color": "#e6edf3", "name": "Text" },
        { "slug": "accent", "color": "#58a6ff", "name": "Accent" },
        { "slug": "success", "color": "#3fb950", "name": "Success" }
      ]
    }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--base)",
      "text": "var(--wp--preset--color--text-primary)"
    }
  }
}

Custom Font Stack With Local Files

Loading fonts locally instead of from Google Fonts is better for GDPR compliance and page speed:

"typography": {
  "fontFamilies": [
    {
      "fontFamily": "\"Source Sans 3\", sans-serif",
      "slug": "body",
      "name": "Source Sans",
      "fontFace": [
        {
          "fontFamily": "Source Sans 3",
          "fontWeight": "400",
          "fontStyle": "normal",
          "src": ["file:./assets/fonts/source-sans-regular.woff2"]
        },
        {
          "fontFamily": "Source Sans 3",
          "fontWeight": "600",
          "fontStyle": "normal",
          "src": ["file:./assets/fonts/source-sans-semibold.woff2"]
        }
      ]
    }
  ]
}

Content Width Adjustment

For a reading-focused blog where you want narrow content with the option to go wide for images:

"layout": {
  "contentSize": "640px",
  "wideSize": "960px"
}

640px keeps lines at roughly 65-75 characters, which is the sweet spot for readability. The wide option at 960px gives enough room for comparison images or code blocks without overwhelming the reader.

Debugging theme.json

When your changes don't show up (and they won't, at least once), here's how to track down the problem.

Validate Your JSON

A single missing comma or extra bracket will silently break your entire file. WordPress won't show an error. Copy your theme.json content into JSONLint and validate it. Better yet, use VS Code with the JSON schema URL in your file. It highlights errors as you type.

Browser Dev Tools

Open your browser's developer tools and check the <head> section. WordPress injects a <style> tag with an ID of global-styles-inline-css. Search for your custom property names there. If they're missing, your theme.json isn't being read correctly.

You can also inspect any element and look at its computed styles. WordPress generates properties like --wp--preset--color--primary that should match what you defined.

WP-CLI

If you have WP-CLI access, this command dumps the merged theme.json output (parent + child + user customizations):

wp theme get-settings --format=json | python3 -m json.tool

This shows exactly what WordPress is working with after merging all layers together.

For the full specification and every available property, the WordPress developer reference for global settings and styles is the authoritative source. It's updated with each WordPress release.

Quick Tip: If you're building a child theme for Twenty Twenty-Five or another default block theme, you only need to include the properties you want to override in your child's theme.json. WordPress merges it with the parent automatically. See our child theme guide for the full workflow.

Build With Block Themes

Our WordPress themes ship with well-structured theme.json files you can learn from and customize.

Browse Themes

Related Guides