All WordPress HTML Templates Forms & Webhooks AI & Tools
WordPress

Block Themes vs Classic Themes: What Actually Changed for Developers

Block themes vs classic themes in 2026: what actually changed for developers, from theme.json v3 and lost hooks to template overrides, locking and when PHP

A real working moment illustrating the theme of an article about block themes vs classic. Wide 16:9 banner, one strong focal point, magazine editorial quality, authentic and unstaged.

The first block theme we shipped at SemiColonWeb took roughly a third longer than the classic build it replaced. The second took about the same. By the fourth we were building marketing sites in half the time, because we’d stopped fighting the editor and started writing theme.json properly.

Most of the block themes vs classic debate online is written by people who have built one demo site. The real differences aren’t “PHP vs HTML files”. They’re about where your extension points moved, what your client can now break at 11pm on a Friday, and which parts of WordPress still refuse to be blocks.

Here’s what actually changed, from a shop that maintains both.

Key Takeaways

  • The structural change is that templates became block markup in templates/.html and parts/.html, and the moment a user edits one in the Site Editor, your file stops being the source of truth: a wp_template post takes over.
  • Your hook surface shrank. Most classic theme hooks lived in template files you controlled; in a block theme you reach for renderblock, blocktypemetadata, wpthemejsondata_theme and the Block Bindings API instead.
  • theme.json (schema version 3) is now where design decisions belong. It generates CSS custom properties, drives editor UI, and since WordPress 6.6 outputs rules at :root :where(...) specificity, which is 0-1-0 and trivially overridable.
  • Full site editing is a governance problem before it’s a technical one. Use templateLock, block locking and settings restrictions in theme.json, or budget for support tickets about a deleted header.
  • Classic and hybrid themes are still the right call for app-like builds, heavy WooCommerce customisation and anything where the template is driven by business logic rather than editorial layout.

Where the markup actually lives now

A classic theme resolves a request through the template hierarchy to a PHP file: single-product.php, archive.php, index.php. That file runs. It calls get_header(), loops, echoes. You own every line.

A block theme resolves the same request to templates/single-product.html, which contains block comment markup, not PHP. WordPress parses that markup into block objects and renders each one through its registered render callback. Your “template” is now data that gets interpreted.

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:query {"queryId":0,"query":{"inherit":true},"layout":{"type":"default"}} -->
	<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"} /-->

Note "inherit":true. That tells the Query Loop to use the main query, which means pregetposts still works exactly as you remember. Custom queries inside a Query Loop do not go through pregetposts; you filter queryloopblockqueryvars instead. That single distinction has cost us more debugging hours than any other block theme quirk.

The bigger shift: the file is a default, not a guarantee. Open the Site Editor, nudge one padding value, hit save, and WordPress writes a wp_template post to the database that shadows your file forever. Deploy a fix to the HTML and nothing changes on the front end. We’ve written up the full resolution order in template hierarchy in block themes, because it is the single thing new block theme developers get wrong.

A real working moment that fits an article section about

Block themes vs classic: the hook surface is the real break

Classic theme development was hook-driven because the templates were PHP. You dropped doaction('themeafter_header') into header.php and every child theme and plugin had a seam to work with.

That seam is gone. There’s no PHP in parts/header.html to hang an action off. What you get instead:

  • renderblock / renderblock_{$name}: filter the rendered HTML of any block, including core ones, with full access to attributes and the parsed block tree.
  • blocktypemetadata: modify a block’s registration (supports, attributes, styles) before it registers, server-side, without touching JS.
  • Block Bindings API (WordPress 6.5+): connect a core block’s attribute to a custom source such as post meta, which removed about 80 percent of our reasons to build a custom block.
  • wpthemejsondatatheme: mutate theme.json programmatically at runtime.

That last one is underused. If your design tokens live in a JSON file generated by your build, you can push them into global styles without hand-syncing two sources:

addfilter( 'wpthemejsondatatheme', function ( $themejson ) {
	$tokens = jsondecode( filegetcontents( getthemefilepath( 'build/tokens.json' ) ), true );

	// update_with() deep-merges on top of the parsed theme.json, so you only
	// need to supply the keys you are overriding.
	return $themejson->updatewith( array(
		'version'  => 3,
		'settings' => array(
			'color' => array( 'palette' => $tokens['palette'] ),
		),
	) );
} );

If you already run a token pipeline, the approach in our design tokens with CSS variables write-up maps onto theme.json almost one to one.

theme.json is where the design decisions moved

In a classic theme, your stylesheet was the contract. In a block theme, theme.json generates the CSS custom properties, decides what controls appear in the editor sidebar, and sets layout widths. Writing CSS before you’ve written theme.json is the most common way to end up with a theme that fights itself.

Three things worth knowing in 2026.

Schema version 3 (WordPress 6.6+) changed the defaults for defaultFontSizes and defaultSpacingSizes. If you bump "version": 3 without setting those explicitly, WordPress stops merging its own preset scales into yours. That’s usually what you want. It also silently removes classes your old content was using, so check before you bump on a live site.

Specificity dropped. Global styles now output as :root :where(.wp-block-button__link){...}, which computes to 0-1-0. A single class in your stylesheet beats it. This fixed years of !important wars, but it also means sloppy utility CSS will now override your carefully set global styles without warning.

Restrict settings for clients. Turning off the custom colour picker and gradient builder is a one-line change and it stops half of all brand drift:

{
	"$schema": "https://schemas.wp.org/trunk/theme.json",
	"version": 3,
	"settings": {
		"appearanceTools": true,
		"color": {
			"custom": false,
			"customGradient": false,
			"defaultPalette": false,
			"palette": [
				{ "slug": "brand", "color": "#0f62fe", "name": "Brand" },
				{ "slug": "ink",   "color": "#101418", "name": "Ink" }
			]
		},
		"layout": { "contentSize": "720px", "wideSize": "1180px" },
		"typography": { "customFontSize": false, "fluid": true }
	}
}

One developer-experience trap: WordPress caches compiled global styles in a transient. If your theme.json edits appear not to take effect, add define( 'WPDEVELOPMENTMODE', 'theme' ); to wp-config.php. We lost an afternoon to this once. Never again.

Per-block stylesheets

Block themes load core block styles individually by default, so ship your block overrides the same way rather than appending to one global file:

add_action( 'init', function () {
	wpenqueueblock_style( 'core/quote', array(
		'handle' => 'canvas-quote',
		'src'    => getthemefile_uri( 'assets/blocks/quote.css' ),
		// Providing 'path' lets WordPress inline the file when it is small enough.
		'path'   => getthemefile_path( 'assets/blocks/quote.css' ),
		'ver'    => wpgettheme()->get( 'Version' ),
	) );
} );

On a landing page using six block types, this routinely cuts tens of kilobytes of unused CSS versus a monolithic style.css. Measure it in the Coverage panel in Chrome DevTools before and after; the number is usually bigger than you expect.

Full site editing is a permissions problem

The pitch for full site editing is that clients can change the header without calling you. The reality on maintenance retainers is that clients will change the header, and then call you.

Lock the structural parts. templateLock: "contentOnly" on a group strips every control except text and media, which is exactly right for a hero or a CTA band:

<!-- wp:group {"templateLock":"contentOnly","lock":{"move":true,"remove":true},"align":"full"} -->
<div class="wp-block-group alignfull">
	<!-- wp:heading {"level":1} -->
	<h1>Edit this headline</h1>
	<!-- /wp:heading -->
</div>
<!-- /wp:group -->

For editors who shouldn’t touch templates at all, remove editthemeoptions from their role. The Site Editor disappears, the post editor stays. Crude, effective, and the approach we default to on any site with more than three content editors.

Patterns are the other half of the governance story. A /patterns directory of PHP files with header comments auto-registers, and if you set Block Types: core/post-content or scope them to a template, editors build pages from approved compositions instead of raw columns. This is also the migration target when you’re pulling a site off a builder, which we covered in moving a client site off a page builder to native blocks.

What still needs PHP

Plenty. Anyone telling you block themes removed the need for PHP has not shipped an e-commerce build.

  • Custom post type archives with real logic. Faceted filtering, geo queries, joins against custom tables. The Query Loop covers editorial listing and stops there.
  • WooCommerce beyond the defaults. Cart and checkout are blocks now and they’re good. Product page customisation still runs through Woo’s own hook system in a lot of real builds, and a hybrid theme with a woocommerce/ template folder is often the pragmatic answer.
  • Anything with wp_mail, REST routes, cron, or third party APIs. That was always plugin territory and still is.
  • Forms. Core block forms remain thin. We either register a block that posts to an third-party endpoint like WebForms or we build a small custom block around a validated markup pattern. The accessibility rules don’t change either way: see form validation that respects users.

Hybrid themes are legitimate and badly under-discussed. A classic theme can declare theme.json, call addthemesupport( 'block-template-parts' ), and get editable header and footer parts while keeping PHP templates everywhere else. That migration path has saved two of our legacy clients an entire rebuild.

Debugging, honestly

Classic themes fail loudly. A fatal error names the file and line. Block themes fail quietly: a block renders empty, a layout class points at .wp-container-core-group-is-layout-9a4f2c, and the inline <style> tag that styles it was generated at render time from an attribute object you can’t see in the DOM.

What helps:

  • wpisblock_theme() as a guard in shared plugin code. Cheap, saves confusion.
  • Read the raw postcontent of the wptemplate post, not the file, when something renders differently in production.
  • Install nothing extra until you’ve checked whether a plugin is injecting via render_block. Block filters are invisible in DevTools. If a site is already carrying 40 plugins, start with a plugin audit before blaming the theme.

Front-end performance between the two is close enough that it rarely decides anything. Block themes generate more inline CSS per page; classic themes typically ship more unused CSS up front. Both lose badly to a page builder. If performance is your reason for migrating, measure first.

How we choose on client work

Block theme by default for content-led sites: marketing, publishing, portfolios, anything where an editor needs to build new page layouts without a developer. Hybrid when there’s an existing classic theme worth keeping and the client only wants header and footer control. Classic when the templates are driven by business rules rather than editorial choice, or when the build is essentially an application wearing WordPress.

If you’re starting fresh and want the block theme structure without assembling it from scratch, CanvasWP ships the theme.json scaffolding, pattern library and locked template parts already wired, which is most of the first week’s work. Our full setup, from local tooling to deployment, is in the modern theme development workflow piece.

Frequently Asked Questions

Can I convert a classic theme to a block theme incrementally?

Yes, and you should. Add a theme.json first to get global styles and editor settings, then addthemesupport( 'block-template-parts' ) to make header and footer editable while the rest stays PHP. Convert templates one at a time by adding the matching HTML file, since a block template file takes priority over the PHP equivalent. The point of no return is the Navigation block, so leave menus until last.

Why don’t my theme.json changes show up?

WordPress caches the compiled global styles. Add define( 'WPDEVELOPMENTMODE', 'theme' ); to wp-config.php during development and the cache is bypassed. If it’s still stale, check whether someone has saved global styles in the Site Editor, because user styles sit above theme styles in the merge order and will not be overwritten by your file.

Do child themes still work with block themes?

They work, but they behave differently. A child theme’s theme.json deep-merges with the parent’s rather than replacing it, and a child templates/single.html overrides the parent file entirely with no partial inheritance. There’s no template-level equivalent of calling the parent function, so for small changes a style variation in styles/ is usually cleaner than a whole child theme.

Is full site editing production-ready for client sites?

For content-driven sites, yes, and has been since roughly WordPress 6.4. The caveat is governance rather than stability: an unlocked Site Editor lets any administrator delete your header template part. Lock structural blocks, restrict editthemeoptions, and ship a pattern library so editors compose from approved pieces.

Where did my action hooks in header.php go?

They no longer exist, because there’s no PHP in parts/header.html. Use renderblock or renderblockcore/template-part to inject markup around a rendered block, wpbody_open for anything immediately after the opening body tag, or register a small dynamic block and place it in the template where the hook used to fire. The last option is the most maintainable because it stays visible to whoever edits the template next.

If you’re deciding this week: look at who edits the site. If the answer is “a marketing person, weekly”, build a block theme and spend your time on theme.json and patterns rather than CSS. If the answer is “only developers, via deploys”, a classic or hybrid theme is still a defensible, maintainable choice in 2026, and nobody serious will argue otherwise. Pick for the editing model, not the trend.

block themes vs classic block themes vs classic themes wordpress convert classic theme to block theme incrementally full site editing client site governance render_block filter replace theme hooks templatelock contentonly block locking wordpress block theme theme.json version 3 wp_enqueue_block_style per block css