We’re going to build a WordPress theme from zero. No shortcuts. No mystery. Just clear steps you can follow, test, and ship. By the end, you’ll have a theme that is clean, fast, and easy to grow. In other words, you’ll know what each file does, how the parts connect, and how to shape the design without breaking the site. Most of all, you’ll feel in control.
We’ll keep the language simple. We’ll move at a steady pace. We’ll use modern WordPress features, like the block editor and theme.json, so your theme works with today’s tools. You’ll see the classic pieces too—like the Loop and template files—because those still matter. Instead of long theory, we’ll build as we go. After more than a few steps, you’ll have something real that you can turn into a product or a portfolio piece.
Let’s begin with a safe setup. Then we’ll make the core templates. Then we’ll polish and release.
1) Plan and Prepare: Tools, Structure, and a Blank Theme
Pick your approach. You can make:
- A block theme (modern): Uses the Site Editor, block templates, and
theme.json. - A classic theme (traditional): Uses PHP templates for structure and the Customizer for some settings.
We’ll blend both as needed. We’ll center on a block-first theme, but we’ll keep classic files for broad support. This gives you the best of both worlds.
What you need:
- A local server (like Local, MAMP, or Wamp) with PHP and MySQL.
- A fresh WordPress install.
- A code editor (VS Code works well).
- A browser with DevTools.
Make the folder:
- Go to
wp-content/themes/. - Create a folder for your theme. Use a short, lowercase name, like
sproutpress(use your own brand). - Inside it, create these files and folders (we will fill them soon):
sproutpress/
style.css
functions.php
screenshot.png
theme.json
templates/
parts/
index.php
Add your theme header. Open style.css and paste:
/*
Theme Name: SproutPress
Theme URI: https://example.com/
Author: Your Name
Author URI: https://example.com/
Description: A clean, fast, block-first theme built from scratch.
Version: 1.0.0
Requires at least: 6.5
Tested up to: 6.6
Requires PHP: 8.0
Text Domain: sproutpress
License: GPL-2.0-or-later
*/
This comment block tells WordPress about your theme. Keep it simple and accurate. The screenshot.png will show in the dashboard; make it 1200×900 and place it in the root.
Register basics in functions.php. Start small:
<?php
/**
* SproutPress functions and definitions
*/
if ( ! defined( 'ABSPATH' ) ) exit;
define( 'SPROUTPRESS_VERSION', '1.0.0' );
add_action( 'after_setup_theme', function() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'responsive-embeds' );
add_theme_support( 'automatic-feed-links' );
add_theme_support( 'html5', [ 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script', 'navigation-widgets' ] );
add_theme_support( 'editor-styles' );
add_theme_support( 'wp-block-styles' );
add_theme_support( 'align-wide' );
register_nav_menus( [
'primary' => __( 'Primary Menu', 'sproutpress' ),
'footer' => __( 'Footer Menu', 'sproutpress' ),
] );
} );
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style( 'sproutpress-style', get_stylesheet_uri(), [], SPROUTPRESS_VERSION );
wp_enqueue_script( 'sproutpress-script', get_template_directory_uri() . '/assets/js/theme.js', [], SPROUTPRESS_VERSION, true );
} );
add_action( 'widgets_init', function() {
register_sidebar( [
'name' => __( 'Sidebar', 'sproutpress' ),
'id' => 'sidebar-1',
'description' => __( 'Main sidebar area.', 'sproutpress' ),
'before_widget' => '<section id="%1$s" class="widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget-title">',
'after_title' => '</h2>',
] );
} );
We added core supports, styles, menus, and a sidebar. We also loaded a script placeholder. You can add a real file later.
Create theme.json. This is the heart of a modern theme. It sets colors, fonts, spacing, and editor rules—without custom CSS hacks. Start lean:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
"settings": {
"appearanceTools": true,
"color": {
"palette": [
{ "slug": "ink", "name": "Ink", "color": "#111111" },
{ "slug": "paper", "name": "Paper", "color": "#FFFFFF" },
{ "slug": "leaf", "name": "Leaf", "color": "#2fbf71" },
{ "slug": "sky", "name": "Sky", "color": "#2684ff" },
{ "slug": "sun", "name": "Sun", "color": "#ffb300" }
]
},
"typography": {
"fontFamilies": [
{ "fontFamily": "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", "name": "System Sans", "slug": "system-sans" }
],
"fluid": true,
"lineHeight": true
},
"spacing": {
"units": [ "px", "rem", "em", "%" ],
"blockGap": "1.2rem"
},
"layout": {
"contentSize": "720px",
"wideSize": "1100px"
}
},
"styles": {
"color": { "text": "var(--wp--preset--color--ink)", "background": "var(--wp--preset--color--paper)" },
"typography": { "fontFamily": "var(--wp--preset--font-family--system-sans)", "lineHeight": "1.6" },
"elements": {
"link": { "color": { "text": "var(--wp--preset--color--sky)" } }
}
},
"customTemplates": [],
"templateParts": [
{ "name": "header", "title": "Header", "area": "header" },
{ "name": "footer", "title": "Footer", "area": "footer" }
]
}
This gives you a base look. You can extend it anytime.
Add the main fallback: In index.php, keep this minimal Loop so the theme is valid and loads:
<?php get_header(); ?>
<main id="site-content">
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<article <?php post_class(); ?>>
<h1><?php the_title(); ?></h1>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile; else : ?>
<p><?php esc_html_e( 'No content found.', 'sproutpress' ); ?></p>
<?php endif; ?>
</main>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
Switch on your theme. Open your site’s dashboard → Appearance → Themes → activate your theme. You’re live in dev.
2) Build the Core: Templates, Parts, Blocks, and Layout
Now we shape the surface of the site. We’ll make header and footer parts, a home template, and key single and archive templates. We’ll also keep the Site Editor in mind, since blocks are the norm now.
Template parts. Create two files:
parts/header.htmlparts/footer.html
In parts/header.html:
<!-- wp:group {"tagName":"header","layout":{"type":"constrained"}} -->
<header class="site-header">
<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between","verticalAlignment":"center"}} -->
<div class="header-inner">
<!-- wp:site-title {"level":2} /-->
<!-- wp:navigation {"menuSlug":"primary"} /-->
</div>
<!-- /wp:group -->
</header>
<!-- /wp:group -->
In parts/footer.html:
<!-- wp:group {"tagName":"footer","layout":{"type":"constrained"}} -->
<footer class="site-footer">
<!-- wp:paragraph --> <p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?>.</p> <!-- /wp:paragraph -->
<!-- wp:navigation {"menuSlug":"footer"} /-->
</footer>
<!-- /wp:group -->
Templates. Create templates files. At minimum:
templates/index.html(the catch-all)templates/front-page.html(for the homepage if a static page is set)templates/home.html(for the blog posts index if posts are your home)templates/single.html(single post)templates/page.html(single page)templates/archive.html(category, tag, date, author archives)templates/404.html
Start with templates/index.html:
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main>
<!-- wp:query {"query":{"perPage":10,"postType":"post"}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt {"moreText":"Read more"} /-->
<!-- wp:separator /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->
Homepage (front-page.html). Keep it focused and brand-led:
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main>
<!-- wp:heading {"level":1} --> <h1>Welcome to SproutPress</h1> <!-- /wp:heading -->
<!-- wp:paragraph --> <p>Fast. Simple. Built your way.</p> <!-- /wp:paragraph -->
<!-- wp:columns -->
<div class="wp-block-columns">
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":3} --> <h3>Latest Posts</h3> <!-- /wp:heading -->
<!-- wp:query {"query":{"perPage":3}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-date /-->
<!-- /wp:post-template -->
</div>
<!-- /wp:query -->
</div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column">
<!-- wp:heading {"level":3} --> <h3>About the Site</h3> <!-- /wp:heading -->
<!-- wp:paragraph --> <p>Say a few words about your brand here.</p> <!-- /wp:paragraph -->
</div>
<!-- /wp:column -->
</div>
<!-- /wp:columns -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->
Single post (single.html).
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main>
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-featured-image /-->
<!-- wp:post-content /-->
<!-- wp:post-terms {"term":"category"} /-->
<!-- wp:post-terms {"term":"post_tag"} /-->
<!-- wp:comments /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->
Pages (page.html) can mirror single.html but without categories and tags.
404 (404.html) can be short:
<!-- wp:template-part {"slug":"header"} /-->
<main class="error-404">
<!-- wp:heading {"level":1} --> <h1>We can’t find that page.</h1> <!-- /wp:heading -->
<!-- wp:paragraph --> <p>Try searching again or head back to the homepage.</p> <!-- /wp:paragraph -->
<!-- wp:search {"label":"Search"} /-->
</main>
<!-- wp:template-part {"slug":"footer"} /-->
Classic fallbacks. It’s smart to keep a few PHP templates for wide plugin support:
header.php,footer.php,sidebar.php(simple wrappers)search.php,archive.php,single.php,page.php(fallbacks if a plugin expects classic templates)
A lightweight header.php:
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<a class="skip-link screen-reader-text" href="#site-content"><?php esc_html_e('Skip to content','sproutpress'); ?></a>
<header class="site-header-php">
<h2 class="site-title"><a href="<?php echo esc_url(home_url('/')); ?>"><?php bloginfo('name'); ?></a></h2>
<?php wp_nav_menu( [ 'theme_location' => 'primary' ] ); ?>
</header>
And a matching footer.php:
<footer class="site-footer-php">
<?php wp_nav_menu( [ 'theme_location' => 'footer' ] ); ?>
<p>© <?php echo date('Y'); ?> <?php bloginfo('name'); ?></p>
</footer>
<?php wp_footer(); ?>
</body>
</html>
Block Patterns (optional but powerful). Patterns let users drop pre-made sections fast. Create a patterns/ folder and add a file like hero.php:
<?php
/**
* Title: Hero Section
* Slug: sproutpress/hero
* Categories: sproutpress
*/
?>
<!-- wp:group {"style":{"spacing":{"padding":{"top":"4rem","bottom":"4rem"}}},"layout":{"type":"constrained"}} -->
<div class="hero">
<!-- wp:heading {"level":1} --> <h1>Grow your story.</h1> <!-- /wp:heading -->
<!-- wp:paragraph --> <p>Clean design. Fast pages. Simple edits.</p> <!-- /wp:paragraph -->
<!-- wp:buttons --><div class="wp-block-buttons">
<!-- wp:button {"text":"Get Started"} /-->
</div><!-- /wp:buttons -->
</div>
<!-- /wp:group -->
Now your theme offers a ready hero block in the editor. That feels premium and saves time.
Global styles with CSS. Keep style.css small. Use it for resets, helper classes, and minor gaps the editor cannot solve:
:root {
--sp-container: clamp(16px, 5vw, 40px);
}
body { margin: 0; }
.site-header, .site-footer { padding: 1rem var(--sp-container); }
.entry-content img { max-width: 100%; height: auto; }
.screen-reader-text { position: absolute; left: -9999px; }
Less CSS is more speed. theme.json handles most design tokens now.
Accessibility first.
- Use proper headings in templates (one
<h1>per page). - Keep color contrast strong (dark text on light background works).
- Provide a skip link (we added one).
- Label menus and search fields.
- Use
aria-labelon navs if needed.
Performance basics.
- Load only what you need.
- Let WordPress handle the title tag.
- Use system fonts for a speedy default.
- Defer heavy scripts (we enqueued in the footer).
- Keep images optimized; WordPress lazy loads by default.
3) Elevate and Ship: Features, SEO, Safety, and Release
Now we add polish. We’ll set smart defaults, improve SEO basics, build trust with security best practices, and prep for real users.
Add theme features that help editors.
Featured images. We already enabled thumbnails. Set sizes to keep control:
add_action( 'after_setup_theme', function() {
add_image_size( 'sproutpress-thumb', 400, 260, true );
} );
Use that size in templates or patterns if you want a uniform card grid.
Custom logo (optional).
add_action( 'after_setup_theme', function() {
add_theme_support( 'custom-logo', [
'height' => 80,
'width' => 240,
'flex-width' => true,
'flex-height' => true,
] );
} );
Then add the Site Logo block in your header part, or output it in header.php.
Navigation and breadcrumbs.
- Menus are registered. Build them under Appearance → Menus (or the Site Editor).
- For breadcrumbs, keep it simple. You can write a basic trail in PHP or use a reputable plugin. For now, rely on clear headings and internal links.
SEO fundamentals baked in.
- Clean titles: We added
title-tagsupport. WordPress sets titles well. - Headings: Use one
<h1>per page. Keep posts structured with blocks like Heading, List, and Group. - Fast pages: Avoid heavy fonts and scripts.
- Readable content area: We set
contentSizeto720pxfor comfortable reading. - Meta and social: A plugin can handle Open Graph tags later if you need richer sharing cards. Your theme should not guess business data.
Archive design that helps users. If you add templates/archive.html, use a simple grid:
<!-- wp:template-part {"slug":"header"} /-->
<main class="archive">
<!-- wp:query-title {"type":"archive"} /-->
<!-- wp:query {"query":{"perPage":12}} -->
<div class="wp-block-query">
<!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
<!-- wp:post-featured-image {"isLink":true} /-->
<!-- wp:post-title {"isLink":true,"level":3} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination /-->
</div>
<!-- /wp:query -->
</main>
<!-- wp:template-part {"slug":"footer"} /-->
Comments with care. Comments can be noisy, but if your site needs them, keep the defaults and style the list lightly. Make sure form labels are clear.
Sidebars and widgets. We registered one. If your design is full-width, you can skip it. If you keep it, place <?php dynamic_sidebar('sidebar-1'); ?> in sidebar.php and add a template part for it where needed.
Block editor experience.
- Add
add_theme_support( 'editor-styles' );(we did). - Create
editor-style.cssto make the editor match the front end. Enqueue it withadd_editor_style( 'editor-style.css' );or throughadd_theme_support('editor-styles')and aneditorStylesentry intheme.json. - Provide Patterns to speed up page builds. Offer a few: hero, feature grid, two-column about, pricing table (if relevant), and a call-to-action.
Safety and maintenance.
- Prefix PHP functions to avoid conflicts (we used
sproutpress/SPROUTPRESS_). - Escape output:
esc_html(),esc_url(),esc_attr(), and friends. - Sanitize inputs if you add settings.
- Avoid direct file access. We guarded with
if ( ! defined( 'ABSPATH' ) ) exit;. - Keep the file tree clean and simple. Fewer moving parts, fewer bugs.
Internationalization (i18n).
- Use translation functions for any visible strings, like
__( 'Sidebar', 'sproutpress' ). - Match the
Text Domainto your theme folder name. - Later, you can generate a
.potfile to support languages.
Starter content (optional). You can provide default pages, widgets, and menus to help new users see a ready layout after activation. This is great for demos and first-time theme users.
Testing checklist.
- Core pages: Home, single post, page, archive, 404.
- Menus: Primary and footer show up right.
- Images: Featured image sizes look sharp and consistent.
- Mobile: Check phones and tablets. Test headings, spacing, and buttons.
- Performance: Open DevTools → Lighthouse. Aim for solid scores but focus on real feel—fast to paint, quick to scroll.
- Editor flow: Open the Site Editor. Edit header, footer, and templates. Make sure nothing breaks.
Package for release.
- Update
style.cssversion (1.0.0 for first release). - Keep your
screenshot.pngcrisp and true to the theme style. - Remove unused files. Keep the tree tidy.
- Zip the theme folder.
- If you plan to share it publicly later, read and follow the WordPress theme guidelines and coding standards. Keep the license open (GPL-compatible) if you want it listed in public places.
Evolving the design with theme.json.
- Add fonts with
fontFamilies(local or web). - Extend color palettes for brand use.
- Set tighter rules per block under
styles.blocksso buttons, headings, and quotes look consistent site-wide. - Tune spacing tokens and fluid typography for smoother scales across screens.
Child themes and customizations.
- If you want to keep this as a parent theme, you or others can build a child theme on top. That way, you can push updates to the parent without losing custom tweaks in the child.
A few tiny upgrades that feel big.
- Add a “Back to Top” button with a small JS snippet and CSS.
- Add a simple card style for Post Lists with a soft border and padding.
- Use wide‐align blocks for full-bleed sections. We enabled align-wide already.
Folder map recap (final):
sproutpress/
style.css
functions.php
theme.json
screenshot.png
index.php
header.php
footer.php
sidebar.php
templates/
index.html
front-page.html
home.html
single.html
page.html
archive.html
404.html
parts/
header.html
footer.html
patterns/
hero.php
assets/
css/
editor-style.css
js/
theme.js
Content design that helps you rank.
- Write posts with clear headings and short paragraphs.
- Use lists for steps and tips (like we’re doing here).
- Add helpful images with alt text that describes the image.
- Link between related posts so readers can keep learning.
- Create a few evergreen pages (About, Contact, Services, or Topics) and keep them tidy.
Why this structure works. You have a modern base (theme.json and block templates) plus classic fallbacks (PHP templates). Editors can use the Site Editor to change headers, footers, and templates without code. Developers can step in with patterns, PHP, and light CSS when needed. Speed stays high because we lean on native tools. And maintenance stays simple because we keep the theme small and focused.
Where to go next.
- Add more patterns to match your niche. Think “FAQ,” “Feature grid,” “Gallery row,” and “Testimonial band.”
- Create custom templates for key landing pages.
- Offer style variations (alt palettes and fonts) so users can switch looks in one click.
- If you serve clients, keep a private “starter content” project with common sections you reuse.
Common gotchas and how we avoid them.
- Bloated CSS: We let
theme.jsondo the heavy lifting. We keepstyle.csssmall. - Missing H1: Every template includes a clear title block.
- Overloaded header: We keep only a logo/title and one navigation. Clean, fast, easy.
- Random fonts: Start with system fonts for speed. Add web fonts later only if needed.
- Hard-coded strings: We used translation functions so text can be localized.
- Unknown breakpoints: We rely on fluid typography and simple layouts that adapt well.
Your build flow in one glance.
- Make the theme folder and files.
- Add the
style.cssheader and a screenshot. - Register supports, menus, and assets in
functions.php. - Create
theme.jsonwith base colors, typography, spacing, and layout. - Add template parts (
header.html,footer.html). - Add templates (
index.html,front-page.html,single.html, etc.). - Keep
index.phpand a few PHP templates as fallbacks. - Add patterns for speed and style.
- Test on mobile and desktop. Check editor flow.
- Zip, version, and share.
A final word about mindset. Your theme is a system, not a pile of files. Each part serves a job: structure, style, content, or motion. When you feel stuck, go back to the goal: fast, simple, clear. Make one change at a time. Test. Then move on. That rhythm builds quality.
Momentum Notes: Keep Building, Keep Simple
You now have a theme that starts fast and grows well. You understand the folder, the templates, the parts, and the theme.json core. You know how to add patterns, how to keep CSS small, and how to guide the editor experience. In other words, you’ve moved from guessing to knowing. And that changes everything.
Let’s keep going together. Add one pattern this week. Add a style variation next week. Write a short guide for your future self about how to update menus and headers. But most of all, enjoy the clean structure. It’s your theme now. You built it. You can shape it any way you like.
Forward Path: Craft, Ship, and Shine
We built from the ground up. We kept the code light. We focused on the editor. We set smart defaults. We protected speed. We lifted accessibility. We left space to grow. After more than a few small wins, you now have a real theme that can power real sites.
This is your path forward:
- Craft patterns that match your niche.
- Ship changes in small, steady releases.
- Shine by keeping pages readable, fast, and kind to users.
You and I just built the base. The rest is creativity. Run with it.

