How to Create a Design System in theme.json: From Tokens to Production

Design systems have transformed how digital products are built. Figma files with shared components, Storybook with documented UI states, style dictionaries with exported tokens, these have become standard tools in modern product teams. WordPress block themes bring this design system thinking directly into the CMS through theme.json.

When you build a design system in theme.json, you are not just defining colors and fonts. You are creating a single source of truth that drives the block editor experience, the front-end rendering, and the client’s ability to make on-brand content decisions, all without writing custom PHP or maintaining parallel stylesheets.

This guide walks through building a real, production-ready design system in theme.json, from the first token definition to deployment on a live site.


theme.json is a JSON configuration file that lives in the root of your WordPress block theme. It defines:

  • Design tokens, colors, typography, spacing, shadows, border-radius values
  • Block settings, which controls appear in the editor for which blocks
  • Block styles, default visual properties applied to blocks globally
  • Custom CSS properties, generated from your token definitions and available site-wide

WordPress reads theme.json on every page load and does two things with it: generates CSS custom properties that apply to the front end, and feeds the data to the block editor so the inspector controls (color pickers, font size sliders, spacing inputs) show only your defined system values.

The result is a design system that is simultaneously a development artifact and an editor constraint. Your tokens are enforced in the admin interface automatically.


Start every theme.json file with the schema reference and version number:

Always use the latest version. Version 3 (current as of WordPress 6.6) supports all modern features including fluid typography and the full spacing scale API. The schema reference enables IDE autocomplete and validation when you are working in VS Code with the JSON schema extension.

The top-level structure of theme.json has four main sections:

Section Purpose Generates
settings Define tokens and control editor UI CSS custom properties + editor controls
styles Apply styles to elements and blocks globally Direct CSS rules
customTemplates Register custom full-page templates Available in Page Attributes
templateParts Register template parts and their areas Available in Site Editor

For a design system, you will work primarily in settings and styles.


Defining the Color Palette

A well-designed color palette in theme.json has three layers:

  1. Brand primitives, the raw color values (your primary blue, secondary green, etc.)
  2. Semantic aliases, purpose-driven names (primary, secondary, accent, surface, foreground, background)
  3. Contextual variants, light/dark/muted variants of key colors

In theme.json, each palette entry needs three properties: slug, color, and name.

The slug becomes the CSS custom property suffix: a slug of “primary” generates --wp--preset--color--primary. The name appears in the editor’s color picker UI. Use clear, purpose-driven names that help content editors make good decisions, not technical names that only developers understand.

Disabling Default WordPress Colors

WordPress ships with a default color palette that conflicts with your custom palette if you do not disable it. Set these options in your settings:

  • "defaultPalette": false, removes WordPress’s default color options from the editor
  • "custom": false, disables the free-form color picker, so editors can only choose from your defined palette

This is critical for design system enforcement. Without these settings, editors can pick any color, breaking brand consistency.

Gradient System

Define gradients the same way as solid colors, with slug, gradient (CSS gradient string), and name. Disable the default gradients with "defaultGradients": false to remove WordPress’s built-in gradient options.

Duotone Presets

Duotone presets define two-color image tinting options. Each entry has a slug, colors array (two colors, shadows and highlights), and name. These appear in the Image block and Cover block for branded image treatments.


Font Families

Register font families with their fallback stack and optional font face declarations for self-hosted fonts:

Each fontFamily entry needs:

  • fontFamily, the CSS font-family value with fallbacks
  • slug, used for the CSS custom property and editor reference
  • name, displayed in the editor’s font picker
  • fontFace (optional), array of @font-face declarations for self-hosted fonts

For self-hosted fonts, include fontDisplay: "swap" to prevent invisible text during font loading. Use variable fonts where possible, a single .woff2 file can cover all weights and styles, dramatically reducing HTTP requests.

Disable the default font families with "defaultFontFamilies": false to keep the editor font picker clean.

Font Size Scale

A semantic font size scale in theme.json defines the available sizes in the editor’s font size picker. With fluid typography enabled, each size can have minimum and maximum values that scale between breakpoints using CSS clamp():

Setting "fluid": true at the typography level enables fluid scaling. Each font size entry can then have a fluid property with min and max values. WordPress generates the appropriate clamp() value automatically.

The generated CSS looks like: --wp--preset--font-size--large: clamp(1.25rem, 1.25rem + ((1vw - 0.48rem) * 0.9615), 1.5rem)

Disable default font sizes with "defaultFontSizes": false.

Line Height and Letter Spacing

Enable custom line height and letter spacing controls in the editor:

  • "customLineHeight": true, adds a line height input to typography controls
  • "customLetterSpacing": true, adds a letter spacing input

Note that these are free-form inputs, not constrained to your system values. If you want to constrain letter spacing to defined values, handle this through block styles in the styles section rather than through settings.


Spacing Scale

WordPress generates a spacing scale from your configuration. The simplest approach uses the built-in scale generator:

Setting "defaultSpacingSize": 1rem, "increment": "1.5", "steps": 7, and "mediumStep": 1.5 generates 7 spacing values using a 1.5x multiplier. The generated values appear in the spacing controls for padding, margin, and gap.

For more control, define individual spacing sizes explicitly:

  • Each size needs a slug, size value (any valid CSS length), and name
  • Use a numeric naming scheme (20, 40, 60, 80) for scalability, leaving room to add sizes between existing ones without renaming
  • Include labels that communicate scale to content editors (XS, S, M, L, XL)

Enabling Spacing Controls

Spacing controls in the editor are disabled by default for most blocks. Enable them with:

  • "padding": true, enables padding control
  • "margin": true, enables margin control
  • "blockGap": true, enables the gap between blocks within a container
  • "units": ["rem", "em", "px", "vh", "vw", "%"], defines available units in the editor

You can also enable or disable these per-block in the blocks section to give finer control over which blocks editors can modify.


The layout settings define the default content width and wide width for your theme:

  • "contentSize", maximum width for normal content (typically 680-760px for readable prose)
  • "wideSize", maximum width for wide-aligned blocks (typically 1100-1400px)

These values generate the --wp--style--global--content-size and --wp--style--global--wide-size custom properties, which WordPress uses internally for alignment controls and which you can reference in your CSS.

Enabling Alignment Controls

Set "wideAligned": true and "fullAligned": true in layout settings to enable wide and full alignment options in the editor. Without these, editors cannot break blocks out of the content column.


Custom Border Controls

Border system tokens in theme.json define standard border radius values and control which border options appear in the editor:

  • Enable "color": true, "radius": true, "style": true, "width": true under border settings
  • Define a "customBorderRadius": true if you want free-form input, or constrain it by not enabling it and instead applying default border radius in your block styles

Shadow Presets

WordPress 6.3+ supports shadow presets in theme.json. Define a shadow library that editors can apply to blocks:

Each shadow entry needs a slug, shadow (CSS box-shadow value), and name. Set "defaultPresets": false to disable WordPress’s default shadows. Shadows are available on blocks like Group and Image that support the shadow control.


The styles section of theme.json applies CSS to your elements and blocks globally. This is where you set default values that reflect your design system.

Global Element Styles

Under styles.elements, you can style standard HTML elements:

  • styles.elements.h1 through h6, heading defaults (font size, weight, line height, margin)
  • styles.elements.p, paragraph defaults
  • styles.elements.a, link color and decoration
  • styles.elements.button, button styles for the Button block
  • styles.elements.heading, applied to all heading levels

Reference your palette tokens directly using the "ref" syntax: "color": {"text": {"ref": "styles.color.text"}}. Or use the CSS custom property notation directly: "var:preset|color|primary".

Per-Block Styles

Under styles.blocks, apply styles per block type. For example, to set a default background on the core/group block, or to style the core/separator block to match your design system:

Block-level styles in theme.json are highly specific, they target blocks globally. For variant-specific styles (like a “dark” version of a Group block), use block style variations registered via PHP and styled in your theme’s CSS.


Style variations let you ship multiple design presets, alternate color schemes, typography combinations, spacing densities, as selectable options in the Site Editor’s Styles panel. Each variation is a partial theme.json file stored in a styles/ directory in your theme.

A variation file only needs to contain the values it overrides. For a dark mode variation, you would only need to redefine the color palette and element color values, all other settings inherit from the base theme.json.

Creating a Style Variation

  1. Create the styles/ directory in your theme root
  2. Create a JSON file named for the variation (e.g., styles/dark.json)
  3. Add the schema reference and version
  4. Override only the values that change from the base theme.json
  5. Add a title property at the top level, this is the name shown in the Styles panel

Variations inherit all base theme.json settings and merge the override values on top. This makes it efficient to maintain: a five-color palette change in the dark variation does not require copying all your typography and spacing definitions.


The blocks section of settings lets you fine-tune which controls appear for specific blocks. This is how you enforce your design system in the editor interface.

Per-Block Control Restrictions

For each block, you can enable or disable specific settings:

  • Disable custom colors on the Paragraph block: blocks["core/paragraph"]["color"]["custom"] = false, editors can only choose from your palette
  • Disable font size customization on the Heading block: blocks["core/heading"]["typography"]["customFontSize"] = false, editors must use your type scale
  • Limit available colors per block: override the palette specifically for certain blocks to restrict options
  • Enable padding only on certain blocks: add padding control to Group blocks but not Paragraph blocks

This granular control is what separates a real design system from a theme with some colors defined. You control precisely what editors can change and where.

Global Disable for Experimental Features

Set "appearanceTools": true at the global settings level to enable a wide range of appearance controls (border, spacing, shadow, etc.) across all blocks with a single flag. This is a convenient shorthand for enabling multiple controls at once, and is typically what you want in a complete design system theme.


Every token you define in theme.json generates a CSS custom property that you can use anywhere in your theme’s CSS:

Token Type CSS Property Pattern Example
Color –wp–preset–color–{slug} –wp–preset–color–primary
Font size –wp–preset–font-size–{slug} –wp–preset–font-size–large
Font family –wp–preset–font-family–{slug} –wp–preset–font-family–body
Spacing –wp–preset–spacing–{slug} –wp–preset–spacing–40
Shadow –wp–preset–shadow–{slug} –wp–preset–shadow–elevated
Gradient –wp–preset–gradient–{slug} –wp–preset–gradient–brand-fade

Reference these properties in any custom CSS your theme adds, either in a style.css file or in the styles.css value within theme.json itself. This keeps your design tokens as the single source of truth for both the editor and the front end.


Before deploying a theme.json-driven design system to production, verify these items:

Token Completeness

  • Every color in your design is defined in the palette, no hardcoded hex values in CSS
  • All fonts used are registered in fontFamilies with proper font-face declarations
  • The spacing scale covers all use cases: micro (4px) to macro (128px)
  • Default WordPress palettes, font sizes, and gradients are disabled where you have custom alternatives

Editor Enforcement

  • Custom color picker is disabled on blocks where off-palette choices would break the design
  • Font size customization is disabled on blocks where only your defined scale should be used
  • Test the editor with a non-developer account to verify what options are actually available

Accessibility

  • All foreground/background color combinations meet WCAG AA contrast ratio (4.5:1 for body text, 3:1 for large text)
  • Focus styles are defined and visible in global element styles
  • Font sizes at the minimum end of your scale are readable without zooming

Performance

  • Self-hosted fonts use variable font format where possible (single file, all weights)
  • Font files use woff2 format only (dropping woff and ttf reduces payload significantly)
  • fontDisplay is set to swap on all font-face declarations
  • Duotone presets use SVG filters, verify they are not duplicating filter SVGs in the page source

Style Variations

  • Each variation is tested by switching to it in the Site Editor and checking the front end
  • Variations include titles that describe the design intent to editors
  • Variation color combinations pass accessibility checks

For production themes with a build pipeline, theme.json fits naturally into the toolchain:

  • Design token sync, export tokens from Figma or your style dictionary tool and generate theme.json’s palette, typography, and spacing sections automatically. Tools like Style Dictionary and Token Transformer can target theme.json as an output format.
  • Validation, use the JSON schema reference for in-editor validation, and add a CI step that validates theme.json against the schema before deployment.
  • CSS generation verification, add a build step that generates the theme.json-derived CSS and checks that all expected custom properties are present.
  • Accessibility testing, use automated contrast checking tools (like axe or Pa11y) as part of your CI pipeline, feeding them your theme.json color combinations.

Common Design System Mistakes in theme.json

Even experienced developers make mistakes when building design systems in theme.json. These are the most common problems and how to avoid them.

Defining too many colors. A color palette with 20 or more entries overwhelms editors and makes consistent design decisions harder, not easier. Most effective design systems use 8 to 12 colors: a primary, secondary, and accent color plus their light and dark variants, a surface color, foreground, background, and one or two utility colors for success, warning, and error states. If you find yourself adding more colors, you probably need to rethink your design rather than expand the palette.

Forgetting to disable WordPress defaults. If you define a custom color palette but forget to set defaultPalette to false, editors see both your custom colors and WordPress’s default colors in the picker. This creates confusion and leads to off-brand color choices. The same applies to font sizes, font families, and gradients — always disable the defaults when you provide custom alternatives.

Using px for font sizes instead of rem. Pixel values do not respect the user’s browser font size preference, which is an accessibility issue. Use rem units for all font sizes in your design system. A user who sets their browser default font size to 20px (for readability reasons) will have that preference ignored by px-based font sizes but respected by rem-based values.

Not testing with real content editors. Developers testing their own theme see the system working perfectly because they understand the intent behind every token name and control. But non-technical editors may not understand what “Surface” or “Accent” means in the color picker. Use descriptive names that communicate purpose: “Card Background” is clearer than “Surface”, and “Call to Action” is clearer than “Accent”. Test your design system with someone who did not build it before shipping.

Hardcoding values in CSS instead of using tokens. Every time you write a raw hex color or pixel value in your theme’s style.css instead of referencing the theme.json token via its CSS custom property, you create a value that is not part of the design system. When a client changes their brand color through Global Styles, the hardcoded values will not update. Use –wp–preset–color–primary instead of #3366cc everywhere.

Ignoring the spacing scale for custom CSS. Your spacing scale generates CSS custom properties (–wp–preset–spacing–40, etc.) that should be used in all custom CSS, not just in the block editor. If your custom CSS uses arbitrary margin and padding values instead of the spacing scale tokens, you lose the consistency that the design system provides.


Frequently Asked Questions

Can I use theme.json without a block theme?

Classic themes can include a theme.json file to configure block editor settings and generate CSS custom properties. However, the full power of theme.json — including Global Styles, the Styles panel in the Site Editor, and style variations — is only available in block themes. Classic themes with theme.json get the design tokens and editor controls but not the user-facing customization interface. If you are building a design system, a block theme gives you the complete experience.

How do I handle dark mode with theme.json?

Create a style variation (styles/dark.json) that redefines your color palette with dark mode values. Users can switch to the dark variation through the Site Editor’s Styles panel. For automatic dark mode based on the user’s system preference, you need custom CSS with the prefers-color-scheme media query that swaps your CSS custom property values. theme.json does not natively support media query-based theme switching, but you can layer it on top of your design system with a small amount of custom CSS in your theme’s style.css file.

What happens when WordPress updates and changes theme.json support?

WordPress maintains backward compatibility for theme.json. Version 2 themes continue to work on newer WordPress versions. When a new version of theme.json is released (like version 3), it adds new capabilities without breaking existing ones. You can migrate your theme.json to the latest version at your own pace. WordPress documents all changes between versions in the Block Theme Handbook, so you can review what is new and decide when to upgrade your theme’s schema version.

How do I share design tokens between theme.json and external tools?

The most common approach is to use Style Dictionary or Token Studio (formerly Figma Tokens) as your source of truth, and generate theme.json as one of the output formats. Style Dictionary supports custom output transforms, so you can write a transformer that outputs the theme.json palette, typography, and spacing sections from your token definitions. This keeps Figma, your design documentation, and your WordPress theme all synchronized from the same token source. Changes to the tokens propagate to all platforms automatically through your build pipeline.


The Result: A Self-Documenting Design System

A well-built theme.json design system is self-documenting in a way that separate design files and styleguides are not. The editor interface shows exactly which colors, fonts, and spacing values are available. Content editors see only the approved options. Developers reference the same tokens in CSS. The design system is enforced at the framework level, not through convention or documentation that people may or may not follow.

This is the real power of theme.json: it closes the gap between design intent and implementation by making the design system executable. Your Figma tokens become WordPress tokens become CSS custom properties, all through a single JSON file.

Start with your color palette and a type scale. Get those right, and the rest of the system follows naturally. For practical context on how this design system applies in real theme development workflows, see our guide on building a block theme from scratch. And for a deep look at how block theme performance relates to your token architecture, the Query Loop block guide shows how your layout system connects to dynamic content displays.

Scroll to Top