
How to Create a WordPress Block Theme from Scratch?
Most WordPress developers learned theming with PHP templates: header.php, footer.php, single.php, and a loop in each. That approach still works, but it locks layout decisions in code that site owners cannot touch. Want a different header on the blog? Edit PHP. Want wider content? Edit CSS and hope nothing else breaks. Block themes flip this around. Templates are made of blocks, design settings live in one JSON file, and everything can be edited visually in the Site Editor, while you still control the defaults in code.
The surprising part is how little a block theme needs. Two files make a valid block theme, and a useful one fits in a dozen. There is no loop to write, no wp_head() call to remember, and no enqueue function for the main stylesheet unless you need one.
This article builds a block theme from an empty folder: the required files, a complete theme.json with colors, typography, and layout, the core templates and template parts, a reusable pattern, optional PHP in functions.php, and the workflow for moving changes made in the Site Editor back into your theme's code.
What Makes a Theme a Block Theme
WordPress treats a theme as a block theme when it contains a templates/index.html file. That is the only hard requirement beyond the style.css header every theme needs. Block themes:
- Use HTML files with block markup for templates instead of PHP files
- Configure design settings and styles in
theme.json - Unlock the Site Editor under Appearance > Editor
- Do not use the Customizer, widgets, or classic menus by default
| Concept | Classic theme | Block theme |
|---|---|---|
| Templates | index.php, single.php | templates/index.html, templates/single.html |
| Header and footer | header.php, footer.php | parts/header.html, parts/footer.html |
| Design settings | add_theme_support() calls, CSS | theme.json |
| Site customization | Customizer | Site Editor |
| Menus | register_nav_menus() | Navigation block |
| Reusable sections | Template parts in PHP | Patterns in patterns/ |
If you maintain a classic theme and want to move it over gradually, see how to convert a classic WordPress theme to a block theme.
The Folder Structure
Here is the theme we will build, called Sajjad Starter with the slug sajjad-starter:
# Theme structure
wp-content/themes/sajjad-starter/
├── style.css
├── theme.json
├── functions.php # Optional
├── screenshot.png # Optional, 1200 x 900 recommended
├── templates/
│ ├── index.html
│ ├── home.html
│ ├── single.html
│ ├── page.html
│ ├── archive.html
│ ├── search.html
│ └── 404.html
├── parts/
│ ├── header.html
│ └── footer.html
└── patterns/
└── hero.php
WordPress picks templates using the same template hierarchy as classic themes, just with .html instead of .php. If single.html is missing, a single post falls back to index.html.
Step 1: Create style.css
The stylesheet header identifies the theme. Create the folder and the file:
/* wp-content/themes/sajjad-starter/style.css */
/*
Theme Name: Sajjad Starter
Theme URI: https://websolutionmaster.com/
Author: Sajjad
Description: A minimal block theme built from scratch.
Version: 1.0.0
Requires at least: 6.6
Tested up to: 7.1
Requires PHP: 7.4
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: sajjad-starter
*/
In a block theme, style.css is often nothing but this header, because styles move into theme.json. Unlike a classic theme, WordPress does not load this file on the front end automatically. If you add CSS rules here, you must enqueue it yourself, which is covered in the functions.php step.
Step 2: Create theme.json
theme.json has two main jobs. settings controls which design options exist in the editor: the color palette, font sizes, spacing scale, and which controls are visible. styles sets the defaults applied to the site and to individual blocks.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"useRootPaddingAwareAlignments": true,
"layout": {
"contentSize": "720px",
"wideSize": "1200px"
},
"color": {
"defaultPalette": false,
"defaultGradients": false,
"palette": [
{ "slug": "base", "name": "Base", "color": "#ffffff" },
{ "slug": "contrast", "name": "Contrast", "color": "#111827" },
{ "slug": "primary", "name": "Primary", "color": "#0a48ac" },
{ "slug": "muted", "name": "Muted", "color": "#f3f4f6" }
]
},
"typography": {
"fluid": true,
"defaultFontSizes": false,
"fontFamilies": [
{
"slug": "system",
"name": "System Sans",
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
}
],
"fontSizes": [
{ "slug": "small", "name": "Small", "size": "0.875rem" },
{ "slug": "medium", "name": "Medium", "size": "1.125rem" },
{
"slug": "large",
"name": "Large",
"size": "1.5rem",
"fluid": { "min": "1.25rem", "max": "1.5rem" }
},
{
"slug": "x-large",
"name": "Extra Large",
"size": "2.5rem",
"fluid": { "min": "1.75rem", "max": "2.5rem" }
}
]
},
"spacing": {
"units": ["px", "rem", "%", "vw"],
"spacingScale": { "steps": 0 },
"spacingSizes": [
{ "slug": "20", "name": "Small", "size": "0.75rem" },
{ "slug": "40", "name": "Medium", "size": "1.5rem" },
{ "slug": "60", "name": "Large", "size": "clamp(2rem, 5vw, 4rem)" }
]
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--system)",
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.65"
},
"spacing": {
"blockGap": "1.5rem",
"padding": {
"left": "var(--wp--preset--spacing--40)",
"right": "var(--wp--preset--spacing--40)"
}
},
"elements": {
"link": {
"color": { "text": "var(--wp--preset--color--primary)" },
":hover": { "typography": { "textDecoration": "none" } }
},
"heading": {
"typography": { "fontWeight": "700", "lineHeight": "1.2" }
},
"h1": {
"typography": { "fontSize": "var(--wp--preset--font-size--x-large)" }
},
"h2": {
"typography": { "fontSize": "var(--wp--preset--font-size--large)" }
},
"button": {
"color": {
"background": "var(--wp--preset--color--primary)",
"text": "var(--wp--preset--color--base)"
},
"border": { "radius": "6px" }
}
},
"blocks": {
"core/site-title": {
"typography": {
"fontWeight": "700",
"fontSize": "var(--wp--preset--font-size--large)"
}
},
"core/post-date": {
"color": { "text": "#6b7280" },
"typography": { "fontSize": "var(--wp--preset--font-size--small)" }
}
}
},
"templateParts": [
{ "name": "header", "title": "Header", "area": "header" },
{ "name": "footer", "title": "Footer", "area": "footer" }
],
"customTemplates": [
{
"name": "page-no-title",
"title": "Page Without Title",
"postTypes": ["page"]
}
]
}
What the key parts do:
version: 3is the currenttheme.jsonschema version. The$schemaline gives you autocomplete and validation in VS Code.appearanceTools: trueturns on border, spacing, link color, and similar controls in one line.layout.contentSizeandwideSizedefine the default content width and the width for wide-aligned blocks.palette,fontSizes,spacingSizesbecome CSS custom properties such as--wp--preset--color--primary, and appear as choices in the editor. SettingdefaultPaletteanddefaultFontSizestofalsehides the core presets so editors only see your brand options.fluid: truemakes font sizes scale between theirminandmaxwithclamp(), without writing media queries.styles.elementsandstyles.blocksset defaults for links, headings, buttons, and specific core blocks.templatePartsandcustomTemplatesregister your parts and extra page templates so they show up with proper names in the editor.
For self-hosted web fonts in theme.json, see how to change fonts in a WordPress block theme using theme.json.
Step 3: Create Template Parts
Template parts are reusable regions, mainly the header and footer. They contain plain block markup:
<!-- wp-content/themes/sajjad-starter/parts/header.html -->
<!-- wp:group {"tagName":"header","style":{"spacing":{"padding":{"top":"var:preset|spacing|40","bottom":"var:preset|spacing|40"}}},"layout":{"type":"constrained","contentSize":"1200px"}} -->
<header
class="wp-block-group"
style="padding-top:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40)"
>
<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between","flexWrap":"wrap"}} -->
<div class="wp-block-group">
<!-- wp:site-title /-->
<!-- wp:navigation {"overlayMenu":"mobile"} /-->
</div>
<!-- /wp:group -->
</header>
<!-- /wp:group -->
<!-- wp-content/themes/sajjad-starter/parts/footer.html -->
<!-- wp:group {"tagName":"footer","backgroundColor":"muted","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60"}}},"layout":{"type":"constrained"}} -->
<footer
class="wp-block-group has-muted-background-color has-background"
style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"
>
<!-- wp:paragraph {"align":"center","fontSize":"small"} -->
<p class="has-text-align-center has-small-font-size">
Built with WordPress and a block theme.
</p>
<!-- /wp:paragraph -->
</footer>
<!-- /wp:group -->
A few rules about block markup:
- The comment delimiters (
<!-- wp:group ... -->) define the block and its attributes in JSON. The HTML between them must match what the block's save function would produce, or the editor shows a validation warning. - Self-closing blocks like
<!-- wp:site-title /-->are dynamic and rendered by PHP, so they need no inner HTML. - Preset references use the
var:preset|spacing|40syntax inside attributes, which WordPress converts to thevar(--wp--preset--spacing--40)CSS variable.
You will rarely type this markup by hand. The usual workflow is to build the layout in the Site Editor, then copy it into your files, covered in Step 7.
Step 4: Create the Templates
The required index.html is the fallback for every view:
<!-- wp-content/themes/sajjad-starter/templates/index.html -->
<!-- wp:template-part {"slug":"header","area":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"queryId":1,"query":{"perPage":10,"postType":"post","inherit":true}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-date /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
<!-- wp:query-no-results -->
<!-- wp:paragraph -->
<p>No posts found.</p>
<!-- /wp:paragraph -->
<!-- /wp:query-no-results -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","area":"footer","tagName":"footer"} /-->
The Query Loop block replaces the PHP loop. "inherit": true tells it to use the main query for whatever page is being viewed, so the same template works for the blog home, archives, and search results.
The single post template displays one post:
<!-- wp-content/themes/sajjad-starter/templates/single.html -->
<!-- wp:template-part {"slug":"header","area":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-date /-->
<!-- wp:post-featured-image {"align":"wide"} /-->
<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
<!-- wp:post-terms {"term":"category"} /-->
<!-- wp:comments -->
<div class="wp-block-comments">
<!-- wp:comments-title /-->
<!-- wp:comment-template -->
<!-- wp:comment-author-name /-->
<!-- wp:comment-content /-->
<!-- /wp:comment-template -->
<!-- wp:post-comments-form /-->
</div>
<!-- /wp:comments -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","area":"footer","tagName":"footer"} /-->
Create page.html the same way without the date, terms, and comments, and 404.html with a heading, a short message, and a Search block. The custom template registered in theme.json lives at templates/page-no-title.html and is the page template minus the Post Title block. Editors can then choose it in the page's Template setting.
Step 5: Add a Pattern
Patterns are pre-designed block layouts that editors insert and then edit freely. Files in the patterns/ folder are registered automatically from their header comment:
<?php
// wp-content/themes/sajjad-starter/patterns/hero.php
/**
* Title: Hero
* Slug: sajjad-starter/hero
* Categories: featured, banner
* Keywords: hero, header, intro
* Block Types: core/post-content
* Viewport Width: 1280
*/
?>
<!-- wp:group {"align":"full","backgroundColor":"primary","textColor":"base","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull has-base-color has-primary-background-color has-text-color has-background" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)">
<!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center"><?php echo esc_html__( 'Build faster with blocks', 'sajjad-starter' ); ?></h1>
<!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center"><?php echo esc_html__( 'A short supporting sentence goes here.', 'sajjad-starter' ); ?></p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
Because pattern files are PHP, you can translate strings and reference theme asset URLs with get_theme_file_uri(). Patterns are covered in depth in what block patterns are and how to create your own.
Step 6: Add functions.php (Optional)
A block theme does not need functions.php. Add one when you need to enqueue extra CSS, register block styles, or remove features:
<?php
// wp-content/themes/sajjad-starter/functions.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Load style.css on the front end and in the editor, for the few rules theme.json cannot express.
*/
function sajjad_starter_enqueue_styles() {
wp_enqueue_style(
'sajjad-starter-style',
get_stylesheet_uri(),
array(),
wp_get_theme()->get( 'Version' )
);
}
add_action( 'wp_enqueue_scripts', 'sajjad_starter_enqueue_styles' );
function sajjad_starter_setup() {
add_editor_style( 'style.css' );
}
add_action( 'after_setup_theme', 'sajjad_starter_setup' );
/**
* Register a custom block style for the Button block.
*/
function sajjad_starter_block_styles() {
register_block_style(
'core/button',
array(
'name' => 'outline-primary',
'label' => __( 'Outline Primary', 'sajjad-starter' ),
'inline_style' => '.wp-block-button.is-style-outline-primary .wp-block-button__link { background: transparent; color: var(--wp--preset--color--primary); border: 2px solid currentColor; }',
)
);
}
add_action( 'init', 'sajjad_starter_block_styles' );
Block themes get several features automatically that classic themes had to declare, including title-tag, post-thumbnails, responsive-embeds, editor-styles, and HTML5 markup. You do not need add_theme_support() calls for those.
Step 7: Activate, Customize, and Export
- Go to Dashboard > Appearance > Themes and activate Sajjad Starter.
- Open Appearance > Editor. You will see Navigation, Styles, Pages, Templates, and Patterns.
- Edit a template or the header visually. Click Save.
Here is the part that confuses many developers: changes made in the Site Editor are saved to the database, not to your theme files. Edited templates become wp_template posts, edited parts become wp_template_part posts, and style changes go to a wp_global_styles post. Your files stay as they were, and the database version takes priority.
To move those changes back into code:
- Use Create Block Theme, the official plugin from the WordPress.org team, which can save Site Editor changes directly into the active theme's files or export a ZIP.
- Or copy the block markup manually: open a template in the Site Editor, switch to the Code editor from the options menu, and paste the markup into the matching file.
After saving changes to files, clear the database customizations so the files take effect. In Appearance > Editor > Templates, open the template's actions menu and choose Reset. Template parts can be reset the same way from the Patterns section.
Common Problems and Fixes
- The theme does not appear as a block theme.
templates/index.htmlis missing or misnamed. The folder must betemplates, plural. - Edits to template files have no effect. A customized version exists in the database. Reset the template in the Site Editor.
- "This block contains unexpected or invalid content" in a template. The hand-written HTML does not match the block's expected markup. Rebuild the section in the editor and copy the generated markup instead of typing it.
- CSS in style.css is ignored. Block themes do not enqueue it automatically. Enqueue it in
functions.phpor move the styles intotheme.json. - theme.json changes do not show. Invalid JSON fails silently. Validate the file in an editor with the
$schemaline, and check whether user styles saved in the Site Editor are overriding your values. - Navigation block shows a fallback menu. It has not been assigned a menu. Select it in the header and create or choose a menu.
WordPress Block Theme FAQ
A style.css file with the theme header and a templates/index.html file. With those two files WordPress recognizes the theme as a block theme and enables the Site Editor. A theme.json file is technically optional but almost always used.
Yes. Templates are HTML, but you can use functions.php for hooks, block styles, and enqueues, and pattern files are PHP so they can translate strings and output asset URLs. Dynamic blocks also render through PHP behind the scenes.
In the database. Templates are saved as wp_template posts, template parts as wp_template_part posts, and global style changes as a wp_global_styles post tied to the theme. The original theme files are not modified unless you export the changes back into them.
Less often. Most customizations can happen in the Site Editor without touching the theme. If you need code changes to a third-party block theme that must survive updates, a child theme still works and can override individual templates, parts, and theme.json values.
No, but it is the easiest way to turn Site Editor changes into theme files, create a blank theme, or export a ZIP. It is maintained by the WordPress.org team and is widely used as a development tool rather than on production sites.
Conclusion
A block theme is mostly configuration and markup. style.css names the theme, theme.json defines the palette, typography, spacing, and block defaults, HTML templates and template parts lay out every view with blocks, and patterns give editors ready-made sections. PHP becomes optional, used only for the few things configuration cannot do.
Build the defaults in code, let site owners adjust them in the Site Editor, and use Create Block Theme or the code editor to bring good changes back into version control. From here, style variations let one theme ship several complete looks, and editing templates in the Site Editor is the day-to-day workflow your clients will use.
Here are some useful references for going deeper on block themes:
- Theme Handbook: Block themes — structure, templates, and how block themes differ from classic themes.
- Theme Handbook: Global Settings and Styles (theme.json) — every settings and styles option.
- Theme Handbook: Templates — template hierarchy, template parts, and custom templates.
- WordPress.org: Create Block Theme plugin — save Site Editor changes to theme files and export themes.
- Block Editor Handbook: theme.json reference — the living schema for the current version.


