WordPress Theme.json Tutorial for Beginners: How to Control Global Styles in Block Themes

What Is theme.json and Why Should You Care?

If you have started building or customizing WordPress block themes, you have probably come across a file called theme.json. This single configuration file is the backbone of modern WordPress theme development. It lets you define global settings and styles for your entire theme without writing a single line of CSS or PHP.

Think of theme.json as your theme’s central control panel. Instead of scattering style rules across multiple stylesheets and template files, you define everything in one structured JSON file. Typography, colors, spacing, layout widths, block-level customizations: it all lives here.

In this WordPress theme.json tutorial, we will walk through every section of the file step by step. By the end, you will have a fully functional theme.json file you can drop into your own block theme and start customizing right away.

Where Does theme.json Fit in Block Theme Architecture?

Before we dive into code, let’s understand where theme.json sits within the WordPress block theme structure.

A minimal WordPress block theme requires just three things:

  1. style.css – Contains the theme header metadata (name, version, author, etc.)
  2. templates/index.html – The main template file built with blocks
  3. theme.json – The configuration file for global settings and styles

Here is what a basic block theme folder looks like:

my-theme/
├── style.css
├── theme.json
├── templates/
│   └── index.html
├── parts/
│   ├── header.html
│   └── footer.html
└── assets/
    └── fonts/

The theme.json file is loaded automatically by WordPress. You do not need to enqueue it or register it with any hook. Simply place it in the root of your theme directory and WordPress takes care of the rest.

The Basic Structure of theme.json

Every theme.json file follows a specific schema. Here is the skeleton you will build upon throughout this tutorial:

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

Let’s break down each top-level property:

Property Purpose
$schema Points to the JSON schema for autocompletion and validation in code editors
version The API version of theme.json (use version 3 for WordPress 6.6+)
settings Defines available options like color palettes, font sizes, spacing scales
styles Applies default styles globally or to specific blocks
templateParts Registers template parts like header and footer
customTemplates Registers custom page templates
patterns References block patterns from the WordPress pattern directory

Step 1: Setting Up Your First theme.json File

Create a new file called theme.json in the root directory of your WordPress theme. Paste in this starter template:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "layout": {
      "contentSize": "800px",
      "wideSize": "1200px"
    }
  },
  "styles": {
    "color": {
      "background": "#ffffff",
      "text": "#1a1a1a"
    }
  }
}

Let’s unpack what this does:

  • appearanceTools: true enables a collection of UI controls in the Site Editor, including margin, padding, block gap, link colors, and more. It is a convenient shortcut so you do not have to enable each one individually.
  • layout.contentSize sets the default content width (the main column).
  • layout.wideSize sets the wide alignment width.
  • The styles section sets a white background and dark text color globally.

Save the file, activate your theme, and open the Site Editor. You should see these settings reflected immediately.

Step 2: Defining a Custom Color Palette

One of the most powerful features of theme.json is the ability to define a custom color palette. This palette appears in the block editor whenever a user selects a color for text, backgrounds, or other elements.

{
  "settings": {
    "color": {
      "defaultPalette": false,
      "palette": [
        {
          "slug": "primary",
          "color": "#1e40af",
          "name": "Primary"
        },
        {
          "slug": "secondary",
          "color": "#9333ea",
          "name": "Secondary"
        },
        {
          "slug": "accent",
          "color": "#f59e0b",
          "name": "Accent"
        },
        {
          "slug": "light",
          "color": "#f8fafc",
          "name": "Light"
        },
        {
          "slug": "dark",
          "color": "#0f172a",
          "name": "Dark"
        }
      ]
    }
  }
}

What Happens Behind the Scenes

WordPress automatically generates CSS custom properties from your palette. For the example above, you get:

--wp--preset--color--primary: #1e40af;
--wp--preset--color--secondary: #9333ea;
--wp--preset--color--accent: #f59e0b;
--wp--preset--color--light: #f8fafc;
--wp--preset--color--dark: #0f172a;

You can reference these variables anywhere in your styles section or even in custom CSS. Setting defaultPalette to false removes the default WordPress colors, giving you full control over the available options.

Disabling Color Features You Don’t Need

If you want to keep designs consistent and prevent users from picking arbitrary colors, you can disable certain color features:

"color": {
  "custom": false,
  "customGradient": false,
  "defaultGradients": false,
  "defaultPalette": false
}

Step 3: Configuring Global Typography

Typography is where theme.json really shines. You can register custom font families, define font size presets, and control line height, letter spacing, and font weight.

Registering Custom Fonts

{
  "settings": {
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "'Inter', sans-serif",
          "slug": "inter",
          "name": "Inter",
          "fontFace": [
            {
              "fontFamily": "Inter",
              "fontWeight": "400",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/Inter-Regular.woff2"]
            },
            {
              "fontFamily": "Inter",
              "fontWeight": "700",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/Inter-Bold.woff2"]
            }
          ]
        },
        {
          "fontFamily": "'Playfair Display', serif",
          "slug": "playfair",
          "name": "Playfair Display",
          "fontFace": [
            {
              "fontFamily": "Playfair Display",
              "fontWeight": "700",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/PlayfairDisplay-Bold.woff2"]
            }
          ]
        }
      ]
    }
  }
}

The file:./ prefix tells WordPress the font file is located relative to your theme’s root directory. Place your .woff2 font files in assets/fonts/ and WordPress handles the @font-face declarations automatically.

Defining Font Size Presets

"typography": {
  "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"
    },
    {
      "slug": "xx-large",
      "size": "2.5rem",
      "name": "Huge"
    }
  ],
  "customFontSize": false
}

Setting customFontSize to false prevents users from typing in arbitrary font sizes, keeping your design system consistent.

Using Fluid Typography

WordPress supports fluid (responsive) font sizes out of the box. Instead of a fixed size, you can provide a fluid range:

{
  "slug": "xx-large",
  "size": "2.5rem",
  "name": "Huge",
  "fluid": {
    "min": "1.75rem",
    "max": "2.5rem"
  }
}

WordPress generates a clamp() function for the CSS, so the font size smoothly scales between the min and max values based on the viewport width.

Step 4: Controlling Spacing

Spacing is another area where theme.json gives you precise control. You can define a spacing scale that users can pick from when adjusting padding, margin, and block gap in the editor.

{
  "settings": {
    "spacing": {
      "units": ["px", "rem", "em", "%"],
      "spacingScale": {
        "steps": 7,
        "mediumStep": 1.5,
        "unit": "rem",
        "operator": "*",
        "increment": 1.5
      }
    }
  }
}

Alternatively, you can define custom spacing sizes manually for more control:

"spacing": {
  "spacingSizes": [
    { "slug": "10", "size": "0.25rem", "name": "Tiny" },
    { "slug": "20", "size": "0.5rem", "name": "Small" },
    { "slug": "30", "size": "1rem", "name": "Medium" },
    { "slug": "40", "size": "1.5rem", "name": "Large" },
    { "slug": "50", "size": "2.5rem", "name": "Extra Large" },
    { "slug": "60", "size": "4rem", "name": "Huge" }
  ]
}

These presets generate CSS custom properties like --wp--preset--spacing--30 that you can use throughout your styles.

Step 5: Applying Global Styles

The settings section defines what is available. The styles section defines what is applied by default. This is where you make your design choices.

{
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--light)",
      "text": "var(--wp--preset--color--dark)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--inter)",
      "fontSize": "var(--wp--preset--font-size--medium)",
      "lineHeight": "1.7"
    },
    "spacing": {
      "blockGap": "var(--wp--preset--spacing--30)",
      "padding": {
        "top": "0",
        "right": "var(--wp--preset--spacing--30)",
        "bottom": "0",
        "left": "var(--wp--preset--spacing--30)"
      }
    },
    "elements": {
      "link": {
        "color": {
          "text": "var(--wp--preset--color--primary)"
        },
        ":hover": {
          "color": {
            "text": "var(--wp--preset--color--secondary)"
          }
        }
      },
      "heading": {
        "typography": {
          "fontFamily": "var(--wp--preset--font-family--playfair)",
          "fontWeight": "700",
          "lineHeight": "1.3"
        },
        "color": {
          "text": "var(--wp--preset--color--dark)"
        }
      },
      "h1": {
        "typography": {
          "fontSize": "var(--wp--preset--font-size--xx-large)"
        }
      },
      "h2": {
        "typography": {
          "fontSize": "var(--wp--preset--font-size--x-large)"
        }
      },
      "h3": {
        "typography": {
          "fontSize": "var(--wp--preset--font-size--large)"
        }
      },
      "button": {
        "color": {
          "background": "var(--wp--preset--color--primary)",
          "text": "#ffffff"
        },
        "border": {
          "radius": "6px"
        },
        "typography": {
          "fontWeight": "700"
        },
        ":hover": {
          "color": {
            "background": "var(--wp--preset--color--secondary)"
          }
        }
      }
    }
  }
}

Notice how we reference the CSS custom properties we defined in the settings section. This keeps everything connected. If you change a color in your palette, it automatically updates everywhere it is used.

Step 6: Styling Individual Blocks

Beyond global styles and elements, you can target specific WordPress blocks. This is incredibly useful for fine-tuning the look of individual components.

{
  "styles": {
    "blocks": {
      "core/quote": {
        "border": {
          "left": {
            "color": "var(--wp--preset--color--primary)",
            "width": "4px",
            "style": "solid"
          }
        },
        "spacing": {
          "padding": {
            "left": "var(--wp--preset--spacing--30)"
          }
        },
        "typography": {
          "fontStyle": "italic",
          "fontSize": "var(--wp--preset--font-size--large)"
        }
      },
      "core/code": {
        "color": {
          "background": "var(--wp--preset--color--dark)",
          "text": "#e2e8f0"
        },
        "border": {
          "radius": "8px"
        },
        "spacing": {
          "padding": {
            "top": "var(--wp--preset--spacing--30)",
            "right": "var(--wp--preset--spacing--30)",
            "bottom": "var(--wp--preset--spacing--30)",
            "left": "var(--wp--preset--spacing--30)"
          }
        }
      },
      "core/navigation": {
        "typography": {
          "fontWeight": "600",
          "fontSize": "var(--wp--preset--font-size--small)",
          "textTransform": "uppercase"
        }
      },
      "core/post-title": {
        "typography": {
          "fontFamily": "var(--wp--preset--font-family--playfair)",
          "fontSize": "var(--wp--preset--font-size--xx-large)"
        }
      }
    }
  }
}

You can find the full list of block names in the WordPress Block Editor Handbook. Each block is referenced with the core/block-name format.

Step 7: Registering Template Parts and Custom Templates

While not strictly related to styling, the templateParts and customTemplates sections of theme.json are essential for a complete block theme setup.

{
  "templateParts": [
    {
      "name": "header",
      "title": "Header",
      "area": "header"
    },
    {
      "name": "footer",
      "title": "Footer",
      "area": "footer"
    },
    {
      "name": "sidebar",
      "title": "Sidebar",
      "area": "uncategorized"
    }
  ],
  "customTemplates": [
    {
      "name": "page-no-title",
      "title": "Page Without Title",
      "postTypes": ["page"]
    },
    {
      "name": "page-full-width",
      "title": "Full Width Page",
      "postTypes": ["page"]
    }
  ]
}

The template part HTML files go inside the parts/ directory, while custom template HTML files go inside the templates/ directory.

The Complete theme.json File

Here is a consolidated, production-ready theme.json file combining everything we covered above. You can copy this into your theme and customize the values:

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "layout": {
      "contentSize": "800px",
      "wideSize": "1200px"
    },
    "color": {
      "defaultPalette": false,
      "custom": false,
      "customGradient": false,
      "palette": [
        { "slug": "primary", "color": "#1e40af", "name": "Primary" },
        { "slug": "secondary", "color": "#9333ea", "name": "Secondary" },
        { "slug": "accent", "color": "#f59e0b", "name": "Accent" },
        { "slug": "light", "color": "#f8fafc", "name": "Light" },
        { "slug": "dark", "color": "#0f172a", "name": "Dark" }
      ]
    },
    "typography": {
      "customFontSize": false,
      "fontFamilies": [
        {
          "fontFamily": "'Inter', sans-serif",
          "slug": "inter",
          "name": "Inter",
          "fontFace": [
            {
              "fontFamily": "Inter",
              "fontWeight": "400",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/Inter-Regular.woff2"]
            },
            {
              "fontFamily": "Inter",
              "fontWeight": "700",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/Inter-Bold.woff2"]
            }
          ]
        },
        {
          "fontFamily": "'Playfair Display', serif",
          "slug": "playfair",
          "name": "Playfair Display",
          "fontFace": [
            {
              "fontFamily": "Playfair Display",
              "fontWeight": "700",
              "fontStyle": "normal",
              "src": ["file:./assets/fonts/PlayfairDisplay-Bold.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" },
        {
          "slug": "xx-large",
          "size": "2.5rem",
          "name": "Huge",
          "fluid": { "min": "1.75rem", "max": "2.5rem" }
        }
      ]
    },
    "spacing": {
      "units": ["px", "rem", "em", "%"],
      "spacingSizes": [
        { "slug": "10", "size": "0.25rem", "name": "Tiny" },
        { "slug": "20", "size": "0.5rem", "name": "Small" },
        { "slug": "30", "size": "1rem", "name": "Medium" },
        { "slug": "40", "size": "1.5rem", "name": "Large" },
        { "slug": "50", "size": "2.5rem", "name": "Extra Large" },
        { "slug": "60", "size": "4rem", "name": "Huge" }
      ]
    }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--light)",
      "text": "var(--wp--preset--color--dark)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--inter)",
      "fontSize": "var(--wp--preset--font-size--medium)",
      "lineHeight": "1.7"
    },
    "spacing": {
      "blockGap": "var(--wp--preset--spacing--30)",
      "padding": {
        "top": "0",
        "right": "var(--wp--preset--spacing--30)",
        "bottom": "0",
        "left": "var(--wp--preset--spacing--30)"
      }
    },
    "elements": {
      "link": {
        "color": { "text": "var(--wp--preset--color--primary)" },
        ":hover": { "color": { "text": "var(--wp--preset--color--secondary)" } }
      },
      "heading": {
        "typography": {
          "fontFamily": "var(--wp--preset--font-family--playfair)",
          "fontWeight": "700",
          "lineHeight": "1.3"
        }
      },
      "h1": { "typography": { "fontSize": "var(--wp--preset--font-size--xx-large)" } },
      "h2": { "typography": { "fontSize": "var(--wp--preset--font-size--x-large)" } },
      "h3": { "typography": { "fontSize": "var(--wp--preset--font-size--large)" } },
      "button": {
        "color": {
          "background": "var(--wp--preset--color--primary)",
          "text": "#ffffff"
        },
        "border": { "radius": "6px" },
        ":hover": { "color": { "background": "var(--wp--preset--color--secondary)" } }
      }
    },
    "blocks": {
      "core/quote": {
        "border": {
          "left": {
            "color": "var(--wp--preset--color--primary)",
            "width": "4px",
            "style": "solid"
          }
        },
        "typography": { "fontStyle": "italic" }
      },
      "core/code": {
        "color": {
          "background": "var(--wp--preset--color--dark)",
          "text": "#e2e8f0"
        },
        "border": { "radius": "8px" }
      }
    }
  },
  "templateParts": [
    { "name": "header", "title": "Header", "area": "header" },
    { "name": "footer", "title": "Footer", "area": "footer" }
  ]
}

How theme.json Interacts with the Site Editor

Everything you define in theme.json serves as the default starting point. Users can then override these defaults through the WordPress Site Editor (Appearance > Editor > Styles). This creates a layered system:

  1. WordPress core defaults (lowest priority)
  2. Your theme.json file (overrides core)
  3. User customizations via Site Editor (highest priority)

This means your theme.json defines the baseline design, but site owners always have the option to adjust things without touching code. If you want to restrict what users can change, you can disable specific settings (like we did with customFontSize: false and custom: false for colors).

Common Mistakes to Avoid

When you are just getting started with theme.json, there are a few pitfalls that can cause frustration:

  • Invalid JSON syntax. A single missing comma or extra trailing comma will break the entire file. Use a JSON validator or a code editor with JSON linting (VS Code works great for this).
  • Wrong version number. If you are using features from WordPress 6.6 or later, make sure you set the version to 3. Older version numbers may not support all properties.
  • Forgetting the $schema line. While technically optional, the schema line enables autocompletion and error checking in your editor. Always include it.
  • Mixing up settings and styles. Settings define what is available. Styles define what is applied. Putting a color palette inside styles will not work.
  • Incorrect font file paths. The file:./ prefix is relative to the theme root. Double-check that your font files actually exist at the specified path.
  • Not clearing caches. If changes are not showing up, clear your browser cache and any server-side caching plugins.

Useful Tools for Working with theme.json

Tool What It Does
VS Code with JSON schema support Autocomplete and validation for theme.json properties
WordPress Theme.json Generator (online tools) Visual interface to generate a theme.json file
Create Block Theme plugin Export Site Editor changes back into your theme.json file
JSONLint.com Quick validation to catch syntax errors in your JSON

What About Classic Themes?

You might be wondering if theme.json works with classic (non-block) themes. The answer is yes, partially. Classic themes can include a theme.json file to opt into certain block editor features like custom color palettes and font sizes. However, the full power of theme.json (template parts, global styles, block-level styling) only works with block themes.

If you are still running a classic theme, adding a theme.json file is still a smart move. It prepares your theme for the future and gives you control over the block editor experience.

Frequently Asked Questions

Do I need to know JSON to use theme.json?

You need a basic understanding of JSON syntax, which is simpler than most programming languages. JSON is just key-value pairs wrapped in curly braces. If you can edit a WordPress config file, you can work with theme.json.

Can I use theme.json and a regular style.css file at the same time?

Yes. The style.css file is still required for the theme header comment (theme name, version, etc.). You can also add custom CSS there, but styles defined in theme.json take priority for block-related styling.

What happens if I make a mistake in theme.json?

If the JSON syntax is invalid, WordPress will ignore the file entirely and fall back to its defaults. Your site will not break, but your custom styles will not appear. Always validate your JSON before saving.

How do I override a parent theme’s theme.json in a child theme?

Create a theme.json file in your child theme. WordPress merges the child theme’s theme.json with the parent theme’s file, with the child taking priority. You only need to include the properties you want to override, not the entire file.

Is there a visual editor for theme.json?

Yes, the WordPress Site Editor acts as a visual interface for many theme.json settings. Additionally, the Create Block Theme plugin lets you make changes visually and then export them back to your theme.json file. There are also online theme.json generators that provide a form-based interface.

What version of WordPress do I need for theme.json version 3?

Theme.json version 3 requires WordPress 6.6 or later. If you are running an older version, use version 2 instead. We recommend keeping WordPress updated to the latest stable release to access all theme.json features.

Can I define custom CSS properties in theme.json?

Yes, you can define custom values using the settings.custom property. For example, setting "custom": { "lineHeightBody": "1.7" } generates the CSS variable --wp--custom--line-height-body: 1.7; that you can use anywhere.

Wrapping Up

The theme.json file is the foundation of modern WordPress theme development. It replaces scattered PHP functions and CSS rules with a single, structured configuration file that is easy to read, maintain, and share.

To summarize what we covered:

  • theme.json controls global settings (what is available) and styles (what is applied)
  • You can define custom color palettes, font families, font sizes, and spacing scales
  • Style individual elements (links, headings, buttons) and specific blocks
  • Register template parts and custom templates for your block theme
  • Everything integrates with the Site Editor, giving users visual control over your defaults

The best way to learn is to start building. Copy the complete theme.json example from this tutorial, drop it into a minimal block theme, and start changing values. You will be surprised how quickly you can create a polished, consistent design without writing any CSS.

Leave a Comment