Block Themes

Block themes replace PHP templates with HTML block markup in templates/ and parts/, edited through the Site Editor.

Syntaxthemes/{slug}/ → style.css, theme.json, templates/*.html, parts/*.html, patterns/*.php, styles/*.json

A 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 as header.html and footer.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

Example · php
<?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.

Example · html
<!-- 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.

Example · html
<!-- 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.

Example · json
{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "customTemplates": [
    {
      "name": "page-no-title",
      "title": "Page without title",
      "postTypes": [ "page" ]
    }
  ]
}

Discussion

  • Be the first to comment on this lesson.