Block Themes
Block themes replace PHP templates with HTML block markup in templates/ and parts/, edited through the Site Editor.
themes/{slug}/ → style.css, theme.json, templates/*.html, parts/*.html, patterns/*.php, styles/*.jsonA block theme builds its pages from block markup in HTML files instead of PHP templates. WordPress treats a theme as a block theme when templates/index.html exists. That one file plus style.css is the minimum, though in practice every block theme also ships a theme.json.
The directories
templates/— full-page templates as HTML block markup.parts/— reusable template parts such asheader.htmlandfooter.html.patterns/— PHP files of block markup, insertable in the editor.styles/— JSON style variations of the theme's global settings.
What changes
The template hierarchy still applies, with the same names: single.php becomes templates/single.html, 404.php becomes templates/404.html. There is no get_header(); a template part is referenced by a block comment. Core emits the document wrapper, wp_head() and wp_footer() for you. editor-styles, responsive-embeds and other supports are on by default.
Where edits go
When a user edits a template in the Site Editor, WordPress forks it into the hidden wp_template post type (wp_template_part for parts). That database copy takes precedence over your file from then on.
Example
<?php
/**
* A block theme still has a functions.php — for anything that must be PHP.
* Layout, colour and typography belong in theme.json instead.
*
* @package MyBlockTheme
*/
defined( 'ABSPATH' ) || exit;
add_action( 'after_setup_theme', function () {
/*
* Block themes already support editor-styles, so add_editor_style() alone
* is enough here — no add_theme_support( 'editor-styles' ) needed.
*/
add_editor_style( 'assets/css/editor.css' );
} );
add_action( 'init', function () {
// A style variation users can pick from the block toolbar (WP 5.3+).
register_block_style(
'core/group',
array(
'name' => 'card', // becomes is-style-card
'label' => __( 'Card', 'myblocktheme' ),
'inline_style' => '.wp-block-group.is-style-card{border:1px solid;padding:1rem}',
)
);
// A category so this theme's patterns group together in the inserter.
register_block_pattern_category(
'myblocktheme',
array( 'label' => __( 'My Block Theme', 'myblocktheme' ) )
);
} );
// Front-end CSS is still enqueued the ordinary way.
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style( 'myblocktheme-style', get_stylesheet_uri(), array(), wp_get_theme()->get( 'Version' ) );
} );When to use it
- Deciding whether to build a new site as a block theme or a classic theme
- Converting a classic header.php into parts/header.html
- Explaining to a client why an edited template stopped picking up deploys
- Adding a block style or pattern category that a block theme's HTML files cannot declare
More examples
templates/index.html and the classic call it replaces
The two template-part comments are the block-theme form of get_header() and get_footer(); slug 'header' loads parts/header.html. The Query Loop block is the equivalent of the classic have_posts()/the_post() loop, and takes the same kind of arguments as WP_Query through its query attribute. Block markup is HTML comments, so the file is still valid HTML if the blocks are stripped.
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"query":{"perPage":10,"postType":"post"}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true,"level":2} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->parts/header.html and theme.json's part metadata
The file name is the slug: parts/header.html is referenced as slug 'header'. Without the templateParts entry in theme.json the part still works, but the Site Editor shows it as a generic 'uncategorized' part with the raw slug as its name.
<!-- parts/header.html -->
<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between"}} -->
<div class="wp-block-group">
<!-- wp:site-title /-->
<!-- wp:navigation {"layout":{"type":"flex"}} /-->
</div>
<!-- /wp:group -->
<!-- theme.json declares the part so the Site Editor labels and groups it:
{
"version": 3,
"templateParts": [
{ "name": "header", "title": "Header", "area": "header" },
{ "name": "footer", "title": "Footer", "area": "footer" }
]
}
-->Registering a custom template users can choose
customTemplates is the block-theme replacement for the classic 'Template Name:' PHP comment. The name must match a file at templates/page-no-title.html, and postTypes controls where the template appears in the editor's Template dropdown. Omit postTypes and it defaults to pages only.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"customTemplates": [
{
"name": "page-no-title",
"title": "Page without title",
"postTypes": [ "page" ]
}
]
}
Discussion