
How to Customize a Block Theme with theme.json in WordPress?
theme.json is the single file that controls a block theme's colors, typography, spacing, and layout, both in the block editor's UI and on the front end, replacing what used to be scattered across add_theme_support() calls, style.css, and inline editor styles. If you're new to block themes generally, what is WordPress Full Site Editing covers the bigger picture first; this guide goes deep on theme.json specifically.
This walks through the file's two main sections (settings and styles), a complete working color palette and typography example, spacing and layout controls, per-block overrides, and how the generated CSS custom properties actually reach the page.
Where theme.json Lives and How It's Structured
theme.json sits at the root of a block theme's folder, alongside style.css and the templates/ directory. Its top level has two main keys:
settingscontrols what options are available to editors, which colors appear in the color picker, which font sizes exist, whether custom colors are even allowed.stylescontrols what the defaults actually are, the base text color, the default heading font size, and so on.
The distinction matters: something can be present in settings (available as a choice) without being applied anywhere by default, and something in styles sets an actual default that renders even if nobody picks anything in the editor.
A Complete Minimal Example
Here's a theme.json defining a real color palette, a type scale, and some baseline styles:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"settings": {
"color": {
"custom": false,
"customDuotone": false,
"palette": [
{ "slug": "primary", "color": "#1e3a8a", "name": "Primary" },
{ "slug": "secondary", "color": "#f59e0b", "name": "Secondary" },
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#18181b", "name": "Contrast" }
]
},
"typography": {
"customFontSize": false,
"fontFamilies": [
{
"slug": "system",
"fontFamily": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
"name": "System Sans"
}
],
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "medium", "size": "1.125rem", "name": "Medium" },
{ "slug": "large", "size": "1.75rem", "name": "Large" },
{ "slug": "x-large", "size": "2.5rem", "name": "Extra Large" }
]
},
"spacing": {
"spacingScale": {
"steps": 5
},
"units": ["px", "%", "em", "rem", "vh", "vw"]
},
"layout": {
"contentSize": "720px",
"wideSize": "1200px"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--system)",
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.6"
},
"elements": {
"link": {
"color": {
"text": "var(--wp--preset--color--primary)"
}
},
"h1": {
"typography": {
"fontSize": "var(--wp--preset--font-size--x-large)"
}
}
}
}
}
A few choices worth explaining:
"custom": falseundercolordisables the arbitrary custom color picker, restricting editors to exactly the palette you defined. This is a deliberate constraint for brand consistency; leave it unset (it defaults totrue) if you want editors to pick any color freely in addition to the palette.contentSizeandwideSizeunderlayoutset the widths that thewp:groupblock's "content width" and "wide width" alignment options use, replacing what used to require custom CSS with a matching class name.elements.linkandelements.h1apply styles to specific HTML elements site-wide, without needing a block-specific override, useful for baseline typographic rules that should apply everywhere.
How This Becomes Actual CSS
WordPress reads theme.json and generates CSS custom properties matching every preset value, following the pattern --wp--preset--{category}--{slug}. The primary color above becomes:
:root {
--wp--preset--color--primary: #1e3a8a;
--wp--preset--color--secondary: #f59e0b;
--wp--preset--font-size--medium: 1.125rem;
}
This is why the styles section above references colors as var(--wp--preset--color--base) rather than hardcoding a hex value again, so if you change the palette entry later, every style referencing it updates automatically, in exactly one place. WordPress also uses the same preset values to generate the corresponding utility classes (has-primary-color, has-primary-background-color) that blocks apply when an editor picks that color in the UI.
Per-Block Settings and Styles
Both settings and styles accept a blocks key for overriding options on a specific block, rather than site-wide:
{
"styles": {
"blocks": {
"core/button": {
"border": {
"radius": "6px"
},
"spacing": {
"padding": {
"top": "12px",
"bottom": "12px",
"left": "24px",
"right": "24px"
}
}
},
"core/quote": {
"typography": {
"fontStyle": "italic"
},
"border": {
"left": {
"color": "var(--wp--preset--color--primary)",
"width": "4px"
}
}
}
}
}
}
This gives every core/button block on the site rounded corners and consistent padding by default, and every core/quote block a colored left border, without writing a single line of custom CSS or touching a template file.
Configuring Spacing and Layout in Detail
Beyond the basic contentSize/wideSize shown earlier, the spacing settings control the padding and margin scale editors see when adjusting a block's spacing controls:
{
"settings": {
"spacing": {
"padding": true,
"margin": true,
"blockGap": true,
"units": ["px", "em", "rem", "%"],
"spacingScale": {
"operator": "*",
"increment": 1.5,
"steps": 7,
"mediumStep": 1.5,
"unit": "rem"
}
}
},
"styles": {
"spacing": {
"blockGap": "1.5rem",
"padding": {
"top": "0",
"right": "1rem",
"bottom": "0",
"left": "1rem"
}
}
}
}
spacingScale generates a full set of preset spacing sizes (--wp--preset--spacing--20, --wp--preset--spacing--30, and so on) following a mathematical progression from a single mediumStep value, rather than requiring you to hand-list every step, useful for keeping spacing visually consistent across an entire site without an editor needing to guess at pixel values. blockGap specifically controls the gap WordPress applies between blocks inside a container that supports it (like core/group with a flex or grid layout), functioning similarly to CSS's gap property.
Defining Custom Templates and Template Parts Areas
theme.json also declares which custom templates and template part areas are available for an editor to assign to a specific page, via customTemplates and templateParts:
{
"customTemplates": [
{
"name": "full-width",
"title": "Full Width",
"postTypes": ["page"]
}
],
"templateParts": [
{
"name": "header",
"title": "Header",
"area": "header"
},
{
"name": "footer",
"title": "Footer",
"area": "footer"
}
]
}
The customTemplates entry above makes a "Full Width" template option available in the page attributes panel for any page post type, matching a templates/full-width.html file in the theme. The templateParts entries associate a named template part with a semantic area (header, footer, or uncategorized), which the Site Editor uses to decide how that part should be labeled and where it's suggested for insertion.
Restricting What Editors Can Change
settings can also disable options entirely, useful for keeping a design consistent across many editors with varying skill levels:
{
"settings": {
"typography": {
"customFontSize": false,
"dropCap": false
},
"color": {
"custom": false,
"customGradient": false,
"customDuotone": false
},
"spacing": {
"customSpacingSize": false
}
}
}
Each of these removes the corresponding freeform control from block toolbars and the settings sidebar, forcing editors to choose from the palette, font sizes, and spacing scale you've explicitly defined, rather than typing arbitrary values.
Locking Settings Per-Block with templateLock and Block Bindings
Beyond global settings, individual blocks inside a template can restrict what's editable using the templateLock attribute on a parent block ("all" prevents adding, removing, or reordering inner blocks; "insert" allows moving but not adding/removing). This is set on the block markup inside a template file rather than in theme.json itself, but it works alongside the same design-token restrictions to keep page structure consistent across a site with many contributors.
Validating Your theme.json
The $schema key at the top of every example in this guide (https://schemas.wp.org/trunk/theme.json) isn't decorative, most modern code editors (including VS Code) use it to provide autocomplete and inline validation against the official schema as you type, catching typos in setting names or malformed nesting before you ever load the file in WordPress. If a theme.json file has invalid JSON syntax, WordPress silently falls back to its default settings rather than showing an error on the front end, which makes editor-level validation via the schema considerably easier to debug against than trial and error in the browser.
Testing Changes Before Shipping Them
A useful workflow when iterating on theme.json: keep a local WordPress environment (via a tool like Local or wp-env) running the exact theme you're editing, and reload the Site Editor after each change rather than editing directly on a production site. Because theme.json affects both the editor's available options and the generated front-end CSS simultaneously, a change that looks fine in the editor's preview is still worth double-checking on an actual published page, particularly for anything touching elements or per-block styles, since specificity between theme.json-generated CSS and a theme's own stylesheet can occasionally produce surprises that only show up outside the editor's iframe.
Frequently Asked Questions (FAQ) About theme.json
For most standard styling, no, colors, typography, spacing, and per-block styles are all expressible in theme.json and get compiled to CSS automatically. Complex layouts, animations, or anything outside what theme.json's schema covers still require a regular stylesheet enqueued alongside the theme.
settings controls what options are available to editors in the block editor UI (which colors, font sizes, and controls appear); styles sets the actual default values applied on the front end and in the editor preview. A color can exist in settings.color.palette without ever being used unless something in styles (or an editor's manual choice) references it.
Partially. WordPress will read a theme.json file in a classic theme and apply its settings and styles to the block editor for post content, but it has no effect on page templates, since classic themes render templates with PHP, not the block-based templates theme.json's layout and template-part features assume.
A child block theme can include its own theme.json, and WordPress deep-merges it with the parent theme's theme.json, with the child's values taking precedence where they overlap. This lets a child theme override just a color palette or a few block styles without redefining the entire file.
Check that it's listed under settings.color.palette with a unique slug, and that settings.color.custom hasn't been left enabled in a way that's hiding your palette behind a "custom color" tab the editor defaults to. Also confirm there's no theme.json in a child theme silently overriding the parent's palette.
Version 1 of the theme.json schema was introduced in WordPress 5.8; version 2 (used throughout this guide, and the current recommended version) landed in WordPress 5.9 alongside the initial Full Site Editing release. Always set "version": 2 in new theme.json files.
Plugins can't edit a theme's theme.json file directly, but they can register additional block support settings or style variations programmatically via PHP hooks like wp_theme_json_data_theme, which merges plugin-provided data into the same system theme.json feeds into.
Conclusion
theme.json centralizes what used to be spread across functions.php, style.css, and ad hoc editor styles into one declarative file, with settings defining what editors can choose and styles defining what actually renders by default. Every value you define becomes both a CSS custom property the whole site can reference and a corresponding option inside the block editor's UI, so there's exactly one place to update a color or font size across an entire site.
Getting comfortable with settings versus styles, the per-block blocks key, and the generated --wp--preset--* custom properties covers the vast majority of what a real block theme needs. From here, building your own custom Gutenberg block is the natural next step once core blocks and theme.json styling stop being enough for a specific piece of custom functionality.
A few additional resources if you want to go deeper:
- WordPress Block Editor Handbook: theme.json Reference — the complete, authoritative schema reference.
- WordPress Block Editor Handbook: Global Settings & Styles — a conceptual guide to how settings and styles interact.
- WordPress Block Editor Handbook: Settings — the full list of configurable settings keys and what each one controls.


