Back to blog

How to Build a Custom WordPress Theme in 2026: A Beginner's Step-by-Step Guide

12 min read
WordPress Theme Development Block Themes Beginner

If you’ve been using WordPress for a while, you’ve probably noticed how much the platform has evolved. Gone are the days when building a WordPress theme meant wrestling with PHP template files and complex theme hierarchies. Welcome to 2026, where creating a custom WordPress theme is more accessible than ever, thanks to block themes and the Full Site Editing (FSE) experience.

In this comprehensive guide, I’m going to walk you through building a custom WordPress block theme from scratch. No prior theme development experience required—just a willingness to learn and experiment.

Why Build a Custom Theme in 2026?

Before we dive into the technical stuff, let’s talk about why you’d want to build a custom theme in the first place.

Complete Control: Pre-made themes are great, but they often come with features you don’t need and lack features you do. Building custom means you get exactly what you want, nothing more, nothing less.

Performance: Custom themes can be incredibly lightweight. You’re not loading bloated CSS frameworks or JavaScript libraries you’ll never use.

Learning Experience: Understanding how themes work makes you a better WordPress developer, even if you primarily work with page builders or existing themes.

Client Work: If you’re doing freelance WordPress work, being able to create custom themes sets you apart from developers who only know how to install pre-made themes.

Future-Proofing: Block themes are the future of WordPress. Learning this now puts you ahead of the curve.

Block Themes vs Classic Themes: Quick Comparison

Let me quickly clarify what we’re building here. WordPress themes come in two flavors:

Classic Themes use PHP template files (header.php, footer.php, single.php, etc.) and rely heavily on functions.php for customization. These have been around since WordPress began.

Block Themes use HTML template files and theme.json for configuration. They’re designed to work seamlessly with the Full Site Editor (FSE) and represent WordPress’s modern approach to theme development.

We’re building a block theme because:

  • They’re easier for beginners (less PHP knowledge needed)
  • They’re more maintainable
  • They leverage the block editor you already know
  • They’re where WordPress is headed

Don’t worry if you’ve never built a classic theme—that’s actually an advantage. You won’t have old habits to unlearn.

Prerequisites and Tools You’ll Need

Before we start coding, make sure you have:

  1. Local WordPress Installation: I recommend Local by Flywheel, XAMPP, or MAMP. You need a development environment where you can safely experiment.

  2. Code Editor: VS Code is my go-to. It’s free, has great extensions for WordPress development, and handles both HTML and JSON beautifully.

  3. Basic HTML/CSS Knowledge: You should understand what tags like <header>, <main>, and <footer> do, and how CSS classes work.

  4. Familiarity with the Block Editor: Spend some time using Gutenberg to create posts and pages. Understanding blocks from a user perspective helps tremendously.

That’s it. Notice what’s NOT on this list: PHP expertise, JavaScript skills, or a computer science degree. Block theme development is genuinely accessible.

Setting Up the Minimum Required Files

Let’s create our theme structure. Navigate to wp-content/themes/ in your WordPress installation and create a new folder. I’ll call mine my-custom-theme, but you can name it whatever you want (use lowercase letters and hyphens, no spaces).

Inside this folder, we need three things to have a valid block theme:

1. style.css

This file contains your theme’s metadata. Create style.css and add:

/*
Theme Name: My Custom Theme
Theme URI: https://yoursite.com
Author: Taufik Hidayat
Author URI: https://yoursite.com
Description: A custom block theme built from scratch
Version: 1.0
Requires at least: 6.4
Tested up to: 6.5
Requires PHP: 7.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-custom-theme
Tags: block-patterns, full-site-editing, custom-colors, custom-menu
*/

This is just metadata—WordPress reads this to identify your theme. The actual styling will come from elsewhere (spoiler: theme.json and your block template files).

2. theme.json

This is the heart of your block theme. Create theme.json in your theme root:

{
  "$schema": "https://schemas.wp.org/wp/6.5/theme.json",
  "version": 3,
  "settings": {
    "appearanceTools": true,
    "layout": {
      "contentSize": "640px",
      "wideSize": "1200px"
    },
    "color": {
      "palette": [
        {
          "slug": "primary",
          "color": "#0073aa",
          "name": "Primary"
        },
        {
          "slug": "secondary",
          "color": "#23282d",
          "name": "Secondary"
        },
        {
          "slug": "white",
          "color": "#ffffff",
          "name": "White"
        },
        {
          "slug": "black",
          "color": "#000000",
          "name": "Black"
        }
      ]
    },
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif",
          "slug": "system",
          "name": "System Font"
        }
      ],
      "fontSizes": [
        {
          "slug": "small",
          "size": "0.875rem",
          "name": "Small"
        },
        {
          "slug": "medium",
          "size": "1rem",
          "name": "Medium"
        },
        {
          "slug": "large",
          "size": "1.5rem",
          "name": "Large"
        },
        {
          "slug": "x-large",
          "size": "2rem",
          "name": "Extra Large"
        }
      ]
    }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--white)",
      "text": "var(--wp--preset--color--black)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--system)",
      "fontSize": "var(--wp--preset--font-size--medium)",
      "lineHeight": "1.6"
    },
    "spacing": {
      "padding": {
        "top": "0",
        "right": "1rem",
        "bottom": "0",
        "left": "1rem"
      }
    }
  }
}

This JSON file does a LOT:

  • Defines your color palette
  • Sets up typography options
  • Establishes spacing and layout defaults
  • Configures what tools appear in the editor

We’ll expand this later, but this gives you a solid foundation.

3. templates/index.html

Block themes use HTML template files instead of PHP. Create a templates folder in your theme, then create index.html inside it:

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date"}} -->
  <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 -->
  </div>
  <!-- /wp:query -->
</main>
<!-- /wp:group -->

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

This looks like HTML comments, but it’s actually WordPress’s block markup. Each comment represents a block. The index.html template is required—it’s the fallback template when no more specific template exists.

Believe it or not, you now have a functional WordPress theme! Let’s activate it and see what we’ve got.

Activating and Previewing Your Theme

Go to your WordPress dashboard → Appearance → Themes. You should see “My Custom Theme” (or whatever you named it). Activate it.

Visit your site. It won’t look like much—white background, black text, basic styling—but it works! You should see your blog posts listed on the homepage.

Now the real fun begins: making it look good and function exactly how you want.

Understanding theme.json in Depth

The theme.json file is your theme’s control center. Let’s explore what you can do with it.

Settings Section

The settings section defines what options are available in the editor. When you set up a color palette, those colors become selectable in the block editor’s color picker. Same with fonts, spacing units, and more.

Enabling Appearance Tools:

"appearanceTools": true

This single line enables border controls, padding, margin, and more in the editor. It’s a shortcut for enabling many individual features.

Layout Settings:

"layout": {
  "contentSize": "640px",
  "wideSize": "1200px"
}

This defines two important widths:

  • contentSize: The default width for content (like blog post text)
  • wideSize: The width when users select “wide width” in the block editor

These create natural content constraints without writing CSS.

Styles Section

The styles section defines your theme’s default appearance. These are the base styles that apply site-wide unless overridden by individual blocks.

"styles": {
  "color": {
    "background": "var(--wp--preset--color--white)",
    "text": "var(--wp--preset--color--black)"
  },
  "typography": {
    "fontFamily": "var(--wp--preset--font-family--system)",
    "fontSize": "var(--wp--preset--font-size--medium)",
    "lineHeight": "1.6"
  }
}

Notice the var(--wp--preset--...) syntax. These are CSS custom properties that WordPress generates from your settings. This creates a system where your settings automatically become usable in your styles.

You can also style individual blocks:

"styles": {
  "blocks": {
    "core/heading": {
      "typography": {
        "fontWeight": "700",
        "lineHeight": "1.2"
      }
    },
    "core/button": {
      "border": {
        "radius": "4px"
      },
      "color": {
        "background": "var(--wp--preset--color--primary)",
        "text": "var(--wp--preset--color--white)"
      }
    }
  }
}

This gives all headings bold weight and tight line-height, and styles buttons with your primary color and rounded corners.

Creating Additional Block Templates

The index.html template works for everything, but you’ll want specific templates for different contexts. Let’s create the most important ones.

templates/single.html

This template displays individual blog posts:

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:post-title {"level":1} /-->

  <!-- wp:group {"layout":{"type":"flex","flexWrap":"wrap"}} -->
  <div class="wp-block-group">
    <!-- wp:post-author {"showAvatar":false} /-->
    <!-- wp:post-date /-->
    <!-- wp:post-terms {"term":"category"} /-->
  </div>
  <!-- /wp:group -->

  <!-- wp:post-featured-image {"height":"400px"} /-->

  <!-- wp:post-content {"layout":{"type":"constrained"}} /-->

  <!-- wp:post-terms {"term":"post_tag"} /-->

  <!-- wp:comments /-->
</main>
<!-- /wp:group -->

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

This creates a nice single post layout with title, meta information, featured image, content, tags, and comments.

templates/page.html

For static pages:

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:post-title {"level":1} /-->
  <!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->

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

Pages are simpler—just title and content, no dates or categories.

templates/archive.html

For category, tag, and date archives:

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:query-title {"type":"archive"} /-->
  <!-- wp:term-description /-->

  <!-- wp:query {"queryId":0,"query":{"perPage":10,"pages":0,"offset":0,"postType":"post","order":"desc","orderBy":"date"}} -->
  <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 -->
  </div>
  <!-- /wp:query -->
</main>
<!-- /wp:group -->

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

templates/404.html

For when pages don’t exist:

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
  <!-- wp:heading {"level":1} -->
  <h1>404: Page Not Found</h1>
  <!-- /wp:heading -->

  <!-- wp:paragraph -->
  <p>Sorry, the page you're looking for doesn't exist. Try searching for what you need:</p>
  <!-- /wp:paragraph -->

  <!-- wp:search {"label":"Search","showLabel":false,"placeholder":"Search..."} /-->
</main>
<!-- /wp:group -->

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

Working with Template Parts

You’ve seen <!-- wp:template-part {"slug":"header","tagName":"header"} /--> in every template. Template parts are reusable chunks—think of them as partials or components.

Create a parts folder in your theme, then create these files:

parts/header.html

<!-- wp:group {"tagName":"header","style":{"spacing":{"padding":{"top":"1rem","bottom":"1rem"}}},"backgroundColor":"white","layout":{"type":"constrained"}} -->
<header class="wp-block-group has-white-background-color has-background">
  <!-- wp:group {"layout":{"type":"flex","flexWrap":"wrap","justifyContent":"space-between"}} -->
  <div class="wp-block-group">
    <!-- wp:site-title {"level":0} /-->
    <!-- wp:navigation {"layout":{"type":"flex","setCascadingProperties":true}} /-->
  </div>
  <!-- /wp:group -->
</header>
<!-- /wp:group -->

This creates a header with your site title and navigation menu, flexbox-aligned with space between them.

parts/footer.html

<!-- wp:group {"tagName":"footer","style":{"spacing":{"padding":{"top":"2rem","bottom":"2rem"}}},"backgroundColor":"secondary","textColor":"white","layout":{"type":"constrained"}} -->
<footer class="wp-block-group has-secondary-background-color has-white-color has-text-color has-background">
  <!-- wp:paragraph {"align":"center"} -->
  <p class="has-text-align-center">© 2026 My Custom Theme. Built with WordPress.</p>
  <!-- /wp:paragraph -->

  <!-- wp:social-links {"iconColor":"white","iconColorValue":"#ffffff","layout":{"type":"flex","justifyContent":"center"}} -->
  <ul class="wp-block-social-links has-icon-color">
    <!-- wp:social-link {"url":"#","service":"twitter"} /-->
    <!-- wp:social-link {"url":"#","service":"github"} /-->
  </ul>
  <!-- /wp:social-links -->
</footer>
<!-- /wp:group -->

This creates a footer with copyright text and social icons, using your secondary color from theme.json.

Adding Custom Fonts and Colors

Let’s make your theme more unique by adding Google Fonts and expanding the color palette.

Adding Google Fonts

Update your theme.json to include Google Fonts:

"typography": {
  "fontFamilies": [
    {
      "fontFamily": "Inter, sans-serif",
      "slug": "inter",
      "name": "Inter",
      "fontFace": [
        {
          "fontFamily": "Inter",
          "fontWeight": "400",
          "fontStyle": "normal",
          "src": ["https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2"]
        },
        {
          "fontFamily": "Inter",
          "fontWeight": "700",
          "fontStyle": "normal",
          "src": ["https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuFuYAZ9hiA.woff2"]
        }
      ]
    }
  ]
}

This loads Inter font in regular and bold weights. Users can now select “Inter” in the typography controls.

Expanding the Color Palette

Add more colors to give users more options:

"color": {
  "palette": [
    {
      "slug": "primary",
      "color": "#0073aa",
      "name": "Primary"
    },
    {
      "slug": "secondary",
      "color": "#23282d",
      "name": "Secondary"
    },
    {
      "slug": "accent",
      "color": "#ff6b6b",
      "name": "Accent"
    },
    {
      "slug": "light-gray",
      "color": "#f5f5f5",
      "name": "Light Gray"
    },
    {
      "slug": "white",
      "color": "#ffffff",
      "name": "White"
    },
    {
      "slug": "black",
      "color": "#000000",
      "name": "Black"
    }
  ]
}

These colors automatically become available throughout the editor.

Creating Custom Block Patterns

Block patterns are pre-designed block layouts that users can insert with one click. They’re incredibly useful and easy to create.

Create a patterns folder in your theme, then create hero-section.php:

<?php
/**
 * Title: Hero Section
 * Slug: my-custom-theme/hero-section
 * Categories: featured
 */
?>

<!-- wp:cover {"url":"<?php echo esc_url( get_template_directory_uri() ); ?>/assets/images/hero-bg.jpg","dimRatio":50,"minHeight":500,"contentPosition":"center center","isDark":true} -->
<div class="wp-block-cover is-light" style="min-height:500px">
  <span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span>
  <div class="wp-block-cover__inner-container">
    <!-- wp:heading {"textAlign":"center","level":1,"fontSize":"x-large"} -->
    <h1 class="has-text-align-center has-x-large-font-size">Welcome to My Site</h1>
    <!-- /wp:heading -->

    <!-- wp:paragraph {"align":"center"} -->
    <p class="has-text-align-center">This is a custom hero section pattern</p>
    <!-- /wp:paragraph -->

    <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
    <div class="wp-block-buttons">
      <!-- wp:button -->
      <div class="wp-block-button"><a class="wp-block-button__link">Get Started</a></div>
      <!-- /wp:button -->
    </div>
    <!-- /wp:buttons -->
  </div>
</div>
<!-- /wp:cover -->

This creates a reusable hero section pattern. Users can insert it from the patterns panel in the editor.

Adding a Theme Screenshot

Create a 1200x900px PNG image showing what your theme looks like and save it as screenshot.png in your theme root. This appears in the Themes admin panel.

Testing Your Theme

Before calling it done, test thoroughly:

  1. Test All Templates: Create a page, a post, visit an archive, trigger a 404
  2. Test in the Editor: Make sure all your colors and fonts show up correctly
  3. Test Navigation: Create a menu and add it using the Navigation block
  4. Test Responsiveness: Check mobile, tablet, and desktop views
  5. Test with Different Content: Long posts, short posts, posts with images, posts without
  6. Accessibility Check: Use WAVE or Lighthouse to check for accessibility issues

Publishing Your Theme

If you want to share your theme:

  1. Clean Up: Remove any test content or debugging code
  2. Document: Create a README.md explaining how to use your theme
  3. Version Control: Put it on GitHub
  4. WordPress.org: If you want to submit to the theme directory, review the theme review guidelines

Next Steps

You’ve built a functional block theme! Here’s what to learn next:

  1. Custom Block Styles: Add custom CSS classes to blocks via theme.json
  2. Block Variations: Create variations of core blocks with predefined settings
  3. Custom Templates: Learn about custom post type templates
  4. Performance Optimization: Minimize CSS, optimize images, implement caching
  5. Advanced theme.json: Explore spacing scales, custom gradients, and duotone filters

Building WordPress themes has never been more accessible. Block themes remove much of the complexity while giving you tremendous control. The best part? As WordPress evolves, your block theme automatically benefits from new features.

Start simple, experiment often, and don’t be afraid to break things (that’s what local development environments are for!). Every professional theme developer started exactly where you are now.

Happy theme building!

— Taufik Hidayat

Share:

Related Posts