In this post, you’ll learn more about how block themes are built. I’ll share what I learned about building a block theme while I was testing Twenty Twenty-Two, the new default theme now shipped with WordPress 5.9, featuring the full-site editing experience. For this walk-through, I ran trunk Twenty-Twenty-Two, with trunk Gutenberg and WordPress 5.8.2.
You can just upgrade to WP 5.9, and the experience should be the same for you. The theme is now available in the WordPress.org Theme Repository for download.
Activating Twenty-Twenty-Two gives us this:
This looks about right. That’s the default Twenty-Twenty-Two home page.
Defining a block theme
All the magic starts in the theme.json file. When this file is present in the active theme, the template editor becomes available. This was a feature added in WordPress 5.8.
Next, the presence of templates/index.html
determines if the site editor interface should be made available in place of the Customizer.
One interesting bit of the functions.php
file is that the developers call add_theme_support( 'wp-block-styles' )
, according to the handbook, to “opt-in to slightly more opinionated styles for the front end”. This will include a couple of block-specific styles listed here. It’s mostly default styling that makes some blocks (e.g. Image) look better.
To demonstrate, I added one Image block and defined the caption. This is how it looks on the editor:
Below you can see how it looks on the live site. Left is without wp-block-styles
. Right is with it included. Notice how the caption is center-aligned, with smaller font size and gray-colored. Using wp-block-styles
makes the end result look exactly like the preview.


The developers enqueued fonts inside the twentytwentytwo_get_font_face_styles
function, which is called when the user opens the editor or the frontend of the site. But make no mistake: all of the styles need to be enqueued for the editor and front end – not just font-face ones. That’s what makes them look the same!
At the end of the functions.php
file, there’s a required call to /inc/block-patterns.php so we can get block patterns registered. More on that later.
How block theming works theme.json
There’s a file called theme.json
that defines some properties, most notably styles and settings. I’ll walk you through all the properties defined in Twenty-Twenty-Two, and there’s a handbook page about it if you want more information.
We’re using version 2 of the specification, which comes with some adjustments, like removing the custom prefix. Version 2 is officially supported on 5.9, matching the theme’s required version.
One of the most important parts of the theme lives inside settings. We define preset color pallette; typography presets (such as font families and sizes); spacing unit options, as well as custom CSS variables that can be used both inside and outside of theme.json
.
All the entries defined in this property are exposed through CSS variables that are used inside stylesheets, block patterns, and blocks themselves.
Aside from presets defined in settings there are also “supports” options to enable or disable certain controls based on the theme developer’s preferences both at the global and individual block levels.
For example, it could be that the content creator should be limited to only selecting from the predefined theme colors and so the custom color control would be disabled in the theme.json settings file with something like this:
{
"version": 2,
"settings": {
"color": {
"custom": false
}
}
}
Code language: JSON / JSON with Comments (json)
The Twenty-Twenty-Two theme doesn’t have many restrictions as it’s meant for the general population. It does turn off DropCaps, though.
{
"version": 2,
"settings": {
"typography": {
"dropCap": false,
…
}
}
}
Code language: JSON / JSON with Comments (json)
There are also many default design control settings inside the styles property. They’re applied to blocks, global color settings, HTML elements, global spacing, and global typography settings. This section gives Twenty-Twenty-Two its unique feeling because here the default design control settings are defined and are enqueued to the CSS that’s pushed to the browser.
Then, one can use all the properties defined in theme.json
inside their style files by using the power of CSS variables, as Gutenberg will inject them into the file. The variable path is a reduction of the actual property path. Something like:
body {
color: var(–wp–preset–color–primary);
}
Code language: CSS (css)
Enables a block to read theme configuration on the live site.
Templates and template parts
The difference between Templates and Template Parts is that the former is a full-page template (be it custom or not), such as home. Both templates and parts can use — block patterns.
Templates are rendered when viewing specific sections of the site (404 pages or post archives are good examples), and obviously, you can define custom templates that might be picked for specific postTypes, such as post or page.
Template parts are almost the same, except that instead of defining a whole page, they are part of a specific section of the template. In Twenty-Twenty-Two’s case, header and footer.
Twenty-Twenty-Two includes a couple of templates and template parts. These are defined inside theme.json
. The purpose of defining templates in theme.json is to determine which post types
the template should be available for use on. For example, the Page (Large Header) template is limited only to pages whereas the Blank template is made available for use as the template for both posts and pages.
Similarly template parts can be defined as intended for use as a header or footer in theme.json
.
Some specifics about the templates in Twenty Twenty-Two:
- 404 uses the header and footer parts, along with a hidden pattern called hidden-404, which cannot be selected inside the Patterns selector;
- archive, is using the Query block just like the home and index templates uses it. The difference between the three is that home has a bigger header than index, and archive has an additional block indicating that the user is browsing the post archives;
- page and single, that have some variations (large header, no separators) but their purpose is to display a page or a single post. The difference between the two is that the page template does not display metadata, such as the date, author and terms;
- There is also a blank template so one could create a page or post from scratch. It only has a post content block. This template allows users to define a single-page website. Here’s an example from the demo that uses it.
The template parts are pretty basic:
- footer, which is basically a Group block with padding and a theme pattern (footer-default) inside;
- header, with the Site Logo and Site Title blocks, and a Navigation block to the right;
- header-small-dark and header-large-dark, that include the header template part — a template part that uses a template part 🤯 — at the top and hidden-bird and hidden-heading-and-bird theme patterns, respectively.
Kjell Reigstad, designer of the Twenty-Twenty-Two theme explained the hidden-heading-and-bird pattern a bit more: “This is a little trick we used to ensure that changes the user makes to the main header and navigation stay in sync throughout their entire site. I don’t think most other themes will need to do this, but it works well for Twenty Twenty-Two.”
How block (and patterns in general) work
Twenty-Twenty-Two has 66 block patterns, so I’m not going to describe them here. Instead, let’s see how they work.
Inside inc/block-patterns.php
, Twenty-Twenty-Two registers block pattern categories using the register_block_pattern_category
function. That function accepts an associative array with the slug of the category, along with its name.

After that, it registers each block pattern by calling register_block_pattern
, passing the slug of the pattern and another associative array with a few properties:
- title;
- categories (at least one of the defined above, except if it’s a hidden pattern);
- blockTypes, that declare in what kind of blocks I can insert this pattern to;
- content of the pattern, formed of one or more blocks.
All that is wrapped into a function that is called after the init action.
One thing that caught my attention is that the contents of the pattern, essentially HTML, is defined inside the PHP file instead of a pure HTML file. I believe that’s for dynamic content (e.g. translations, dynamic URLs). There are plans to change how that works and make it a better developer experience, though! Stay tuned.
The WordPress Block Patterns Resource List has a lot more information about Block Patterns.
Putting it all together
Next Gutenberg kicks in. It is the glue that applies the theme definitions, its overrides, and injects the blocks into the used templates.
When you override a global style that is defined in theme.json or by WordPress core, a new post with the wp_global_styles
type is created. The differences from the original global styles are saved as a custom post type. Here’s what happens when you modify global typography settings:
MariaDB [wordpress]> select post_content
from wp_posts
where post_type = 'wp_global_styles';
post_content |
---|
{“styles”:{“typography”:{“fontSize”:”var:preset|font-size|huge”}},”isGlobalStylesUserThemeJSON”:true,”version”:2} |
Blocks are defined as HTML comments, such as:
<!-- wp:spacer {"height":112} -->
<div style="height:112px" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer -->
Code language: HTMLBars (htmlbars)
The first part defines the block name, and the object inside the comment stands for the block properties. From there, you can add, modify, and delete blocks inside the editor. The changes are saved within the chosen template. The contents that are “wrapped” by comments are the actual markup bits that are rendered on the live site. Of course, those comments are filtered away before the content hits the site’s front end.
When modifying the contents of a template (i.e. adding, removing, or modifying blocks), you get a full copy of the template as a new post with the wp_template type, along with all your changes. When changes are cleared, meaning the template is reverted to its original form, the post is deleted and the original template is used instead.
When a user modifies a block that also touches a WordPress General Setting options, like the for “Site Title” block, the site_title option is also updated in the options table.
MariaDB [wordpress]> SELECT option_value
FROM wp_options
WHERE option_name = 'blogname';
option_value |
---|
This is changed from the editor! |
The same thing happens for posts. When you change the contents of a Post Title block, the post itself is changed. Imagine the following post, inside the site editor:
When you change “Hey there!” to “Something else”, that’s what you would get inside the tables:
MariaDB [wordpress]> SELECT post_title
FROM wp_posts
WHERE post_type = 'post';
post_title |
---|
Something else |
Pretty magical ✨
On a side note: I’m uncertain whether it is a good option to let such changes happen. People might mistakenly assume that this is part of the template and not part of the post itself.
After the changes are saved and the site looks how the user wants, it’s rendering time!
The following paragraphs are an oversimplification of the process, but they should give you a nice overview.
First thing, the gutenberg_maybe_inline_styles()
function is enqueued within wp_head
and wp_footer
actions so that styles are injected. This is where most of the styling, including block styles, global styles, and their overrides are applied. Something similar happens for scripts and external stylesheets. Note that, at this moment, CSS is not yet sent to the browser.
Then, gutenberg_add_template_loader_filters
hooks into the wp_loaded
action. Inside that function, a for-loop enqueues a filter that runs for every possible template:
add_filter( str_replace( '-', '', $template_type ) . '_template', 'gutenberg_override_query_template', 20, 3 );
Note how it calls gutenberg_override_query_template
as a target. Inside it, the corresponding template for the page the user is requesting is resolved and the template-canvas.php
file is returned as the result of the filter. That file is the final bit:
<?php $template_html = gutenberg_get_the_template_html();?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<?php echo $template_html; ?>
<?php wp_footer(); ?>
</body>
</html>
Code language: PHP (php)
As shown inside the code, it calls gutenberg_get_the_template_html
. That function does a few things, but most importantly:
do_blocks
, which grabs all blocks, parses them and return their content;wptexturize
`, to escape all special characters into their safe HTML entities.
And that’s it! gutenberg_get_the_template_html
‘s result gets echoed inside the <body> tag, wp_head and wp_footer functions render their respective content and voilà: your page gets loaded!
That’s how FSE with Gutenberg works, in an oversimplified way in January 2022.
Resources
- A Deep Dive into Twenty Twenty-Two and WordPress Block Themes
- Theme-focused changes and filters in WordPress 5.9
- State of the Customizer with block themes in WordPress 5.9
- Block themes, a new way to build themes in WordPress 5.9
- Updates for Settings, Styles, and theme.json
- Gutenberg Aren’t HTML by Dennis Snell
Big Thank you to Daisy Olsen, Kjell Reigstad, and Jeff Ong for their review of this post.