Building scalable design systems in WordPress with Gutenberg 2026

Building scalable design systems in WordPress with Gutenberg 2026

Last verified: September 22, 2026
7 min read
Guide
Full-stack developer
UI/UX designer

A WordPress design system is the set of tokens, allowed editor controls, and insertable layouts that Gutenberg will actually enforce. Figma files do not enforce anything on the published site. In 2026 that contract is theme.json version 3, the /patterns directory, and a small number of custom blocks. Everything else is a prototype or a database object you have to treat as such.

We keep seeing the same failure on large editorial sites: the brand book lives in Figma, the CSS lives in five stylesheets, and the editor still offers a custom color picker. Marketing then ships a campaign hex that exists on one landing page. The next campaign copies it. Six months later nobody can say which green is the brand green. Gutenberg can prevent that, but only if you turn the controls off and put the palette in theme.json.

This is not a page-builder migration sermon. If you are choosing Gutenberg against Elementor or Divi, read the 2026 builder comparison. If you are choosing a block theme against a classic PHP theme, read classic vs block themes. This piece is the layer after that choice: how the system stays consistent once editors are in the inserter every day.

#theme.json version 3 is the contract

The living specification is theme.json version 3. WordPress 6.6 introduced it. Older version: 2 files still load. New settings land on version 3. There is no version 4 in that spec, despite what a lot of 2025 roundups claimed.

Point the file at the schema that matches the minimum WordPress you support, not at trunk, unless the site always runs the Gutenberg plugin:

{
  "$schema": "https://schemas.wp.org/wp/6.6/theme.json",
  "version": 3,
  "settings": {},
  "styles": {}
}

settings is what the editor is allowed to do. styles is the default look when nobody overrides a block. Mixing those two is how teams accidentally ship a pretty default that editors can still break with one click.

For a site that must stay on-brand, the first settings block we write looks like this:

{
  "version": 3,
  "settings": {
    "color": {
      "custom": false,
      "customDuotone": false,
      "customGradient": false,
      "defaultPalette": false,
      "defaultGradients": false,
      "defaultDuotone": false,
      "palette": [
        { "slug": "base", "name": "Base", "color": "#ffffff" },
        { "slug": "contrast", "name": "Contrast", "color": "#111111" },
        { "slug": "primary", "name": "Primary", "color": "#0b3d91" }
      ]
    },
    "typography": {
      "customFontSize": false,
      "dropCap": false
    }
  }
}

color.custom: false is the old add_theme_support( 'disable-custom-colors' ). defaultPalette: false hides the core gray-and-blue swatches so the picker only shows your slugs. The mapping is in the Global Settings and Styles handbook. If those two flags stay at the defaults (true), you do not have a design system. You have a suggestion.

appearanceTools: true is not a lockdown switch. It turns on border, margin, padding, sticky, min-height and line-height controls. Use it when layout work belongs in the editor. Leave it off, or set the individual keys, when you want spacing to come only from presets.

#Presets become CSS variables you can actually call

Each palette slug becomes a custom property --wp--preset--color--{slug} and utility classes such as .has-{slug}-color and .has-{slug}-background-color. Font sizes and spacing sizes follow --wp--preset--font-size--{slug} and --wp--preset--spacing--{slug}. That naming is in the same handbook. Custom CSS in the theme should use those variables, not a second set of --brand-primary tokens that drift.

That is the whole “design tokens in Gutenberg” story. WordPress does not import Tokens Studio. It emits presets from theme.json. If the design team already has tokens in JSON, the honest pipeline is a build step that writes settings.color.palette (and the type and spacing scales) into theme.json. The deploy then ships one file. A webhook from Figma that “pushes production” without that write is a demo, not a system.

Child themes merge theme.json on top of the parent. Style variations live as extra JSON files under /styles. PHP can still amend the tree with the wp_theme_json_data_theme filter when a network site needs a different primary without forking the theme. Those three splits are what core gives you. Compiling twelve JSON partials in a Node task is fine if your team already owns that task. It is not required, and it is not documented as a WordPress feature.

#Theme patterns are not synced patterns

This is the distinction that burns projects.

Theme Handbook: Introduction to Patterns is blunt: patterns you put in /patterns are not synced. They are templates for the next insert. WordPress copies the block markup into the post. Edit the PHP file later and existing pages do not change. There is an open Gutenberg ticket to ship synced patterns from theme files. It is not how core works today.

Synced patterns (reusable blocks before WordPress 6.3) are wp_block posts in the database. You create them in the editor. Every instance stays in lockstep. WordPress 6.6 added overrides on synced patterns, so a card can keep shared design while the heading and image vary per instance. Those objects are still not in Git. Staging and production each need the same wp_block row, or you recreate them by hand.

Use that split on purpose:

KindLives inUpdates existing pagesShip across environments
Theme pattern (/patterns)the theme, in Gitnoyes, with the deploy
Synced patternthe databaseyesonly if you copy wp_block data
Template part (header/footer)theme file until edited, then DByes, as the partfile until someone customizes it

Heroes, case-study layouts, and landing-page skeletons belong in /patterns. A legal disclaimer, a cookie-line, or a banner that legal can rewrite this afternoon belongs in a synced pattern, with a documented export so staging is not a surprise.

Register theme patterns by dropping a PHP file with a file header into /patterns, which is the path the registering patterns chapter recommends, or by calling register_block_pattern() on init. One pattern, one method. Do not register the same slug twice.

#Lock templates the same way you lock colors

A palette with custom: false still fails if the page template is an empty Group and the inserter offers every core block. For page types that must stay on-model (product, location, documentation), lock the template: allowedBlocks on the group, templateLock: "insert" or "all" where the layout must not be rearranged, and a pattern as the default content so the editor never starts from a blank canvas.

That is also where custom blocks earn their keep. Core Heading plus Paragraph plus Image covers most marketing modules. A comparison table that must carry a SKU, a currency, and a “last verified” date needs attributes and block.json validation. We build those with @wordpress/scripts, keep the save markup boring, and refuse to recreate a page builder inside the inspector. If the block is a skin around inner blocks, prefer a pattern.

#What we actually run on client sites

On WordPress development work the sequence is the same every time:

  1. Tokens in theme.json version 3, picker locked, core palette hidden.
  2. A short pattern set in /patterns for the layouts marketing actually repeats.
  3. One or two synced patterns for copy that legal owns, exported with the content sync, never left as “we’ll recreate it on prod”.
  4. Custom blocks only after a pattern on core blocks has failed a real attribute constraint.
  5. A review in the Site Editor with an editor account, not an administrator account, because admins still see tools you thought you hid.

If the front end already uses utility CSS, keep theme.json as the token source and map slugs into that CSS. We wrote that split down for Tailwind in Harnessing Tailwind CSS for WordPress development. Gutenberg does not care which CSS framework consumes --wp--preset--color--primary. It cares that the editor and the front name the same slug.

The system is working when a new landing page is a pattern insert, the color picker has eight swatches, and a request for “just this one orange” is a theme.json pull request, not a hex in a Group block.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

Article FAQ

Frequently asked questions

Practical answers to apply the topic in real execution.

SEO-readyGEO-readyAEO-ready4 Q&A
Why not keep the design system in Figma and skip theme.json?#
Figma is the prototype. theme.json is what Gutenberg and the front end actually read. There is no official Figma-to-WordPress sync in core. If a token must survive a deploy, it belongs in theme.json (or a build that writes theme.json), not in a Figma comment.
Is theme.json hard to maintain on a large site?#
One 2 000-line file is hard. Core already splits some of that: style variations under /styles, child-theme merge, and the wp_theme_json_data_theme filter. A custom compile of JSON partials is a team choice, not a WordPress feature.
Do custom blocks still belong in a design system?#
Yes, when a layout needs attributes you can validate. A product comparison grid with a required SKU field is a block. A hero with three heading levels and a brand color is a pattern sitting on core blocks.
If I edit a pattern in the theme, will every page update?#
No. Theme-registered patterns are unsynced copies. Changing the file changes the next insert, not the blocks already on posts. For a legal footer that must update everywhere, create a synced pattern in the editor (it lives in the database) and accept that it is not in Git.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles