14+ years building on WordPress / Replies in under 5 hours
Page Builders & Theming 18 min read · Updated July 2026

How to Build a Custom WordPress Theme from Scratch: A Complete Developer’s Guide

AK
Ajay Khandal
WordPress Developer
Illustration of WordPress theme template files (style.css, functions.php, header.php) assembling into a browser window layout, representing building a custom theme from scratch
TL;DR

A custom WordPress theme requires at minimum two files: style.css (containing the theme header comment) and index.php (the fallback template). For a production classic theme you also need functions.php, header.php, footer.php, and template files for different content types (single.php, page.php, archive.php). Block themes replace PHP templates with HTML files in a templates/ folder and use theme.json for global design settings instead of the Customizer. Block themes are the better choice for new projects — they perform better by default, integrate with WordPress's Full Site Editor, and require less PHP. Classic themes remain the right call if your workflow depends on Elementor, Divi, or another page builder, or if you're maintaining an existing codebase.

Building a custom WordPress theme from scratch gives you something no commercial theme or page builder can: complete control over every line of HTML, CSS, and PHP that touches your site. No unused feature bloat, no plugin conflicts from pre-bundled add-ons, no design constraints imposed by someone else’s layout decisions.

WordPress powers over 43% of all websites globally. The majority run on commercial themes or page builders that trade performance for convenience. A well-built custom theme consistently outperforms them — lighter payloads, cleaner markup, and better Core Web Vitals scores that translate directly into search rankings and conversion rates.

This guide walks you through the complete process: environment setup, template hierarchy, building both a classic PHP-based theme and a modern block theme, adding advanced functionality, and deploying to production. If you’d rather hand this off to a specialist, the custom theme development service covers exactly this scope.

What Is a Custom WordPress Theme?

A WordPress theme is a collection of PHP, HTML, CSS, JavaScript, and JSON files that controls the front-end appearance and behaviour of a WordPress site. WordPress loads the right template file based on the page being requested — a post, an archive, a search result — following a specific hierarchy of file names.

The minimum viable theme is two files:

  • style.css — contains the theme header comment that identifies it to WordPress
  • index.php — the fallback template for all content types

Production themes contain multiple template files (single.php, page.php, archive.php), a functions.php for registering features and enqueuing assets, and dedicated header and footer partials.

Why Build a Custom Theme Instead of Using a Pre-Made One?

  • Performance. Commercial themes ship with dozens of layout options, shortcodes, and bundled plugins you’ll never use. Every unused feature is dead weight on every page load. Custom themes contain only what the site needs.
  • Core Web Vitals. Custom themes typically achieve 30–50% better Core Web Vitals scores than bloated commercial alternatives. LCP, CLS, and INP all improve when the HTML is semantic and the asset footprint is minimal.
  • SEO. Clean, semantic HTML is the structural foundation that SEO plugins (Rank Math, Yoast) build on. A custom theme gives them something solid to work with.
  • Security. Every bundled feature in a commercial theme is a potential attack surface. Custom themes have a drastically reduced surface area.
  • Longevity. A commercial theme’s roadmap is someone else’s decision. A custom theme is yours to maintain and evolve.

Classic Theme vs Block Theme: Which Should You Build?

WordPress now has two distinct theme architectures. Choosing the wrong one adds unnecessary friction to your entire project.

Feature Classic Theme Block Theme
Templates PHP files HTML files with block markup
Design settings Customizer + PHP theme.json
Site Editor support No Yes (full FSE)
PHP required Yes Minimal (optional)
Page builder compat Good Limited
Performance baseline Good (with effort) Excellent (by default)
WP development focus Maintenance mode Active (FSE is the future)

Choose a classic theme when:

  • Your workflow depends on Elementor, Divi, Beaver Builder, or another page builder — see page builders vs custom code for a detailed breakdown of the tradeoffs
  • You’re maintaining or extending an existing classic codebase
  • The project requires complex PHP-based view logic that doesn’t map cleanly to blocks

Choose a block theme when:

  • This is a new project where the client will manage design via the Site Editor
  • Performance and Core Web Vitals are a priority
  • You want to align with WordPress’s active development direction — see the Full Site Editing guide for a deeper look at what FSE unlocks

Prerequisites: What You Need Before You Start

  • HTML and CSS — essential for both theme types
  • Basic PHP — needed for classic themes; optional but useful for block themes
  • JavaScript fundamentals — for interactive elements and custom Gutenberg blocks
  • Command line basics — for local environment management
  • Git — strongly recommended; theme files should be version-controlled from day one

Step 1: Set Up Your Local Development Environment

You need a local WordPress installation before writing a single line of theme code. Your options:

  • Local by WP Engine — the fastest setup; one-click WordPress installs, built-in SSL, no manual server configuration
  • @wordpress/env (wp-env) — Docker-based, requires Node.js; best for teams using version-controlled environments
  • MAMP / XAMPP — traditional PHP/MySQL stack; more configuration work but widely understood

Once WordPress is running locally, create your theme folder inside wp-content/themes/:

wp-content/
  themes/
    your-theme-name/
      style.css
      index.php

Recommended VS Code extensions: PHP Intelephense (PHP autocomplete), WordPress Snippets, Prettier, GitLens.

Step 2: Understand the WordPress Template Hierarchy

WordPress uses a specific file-naming convention to determine which template to load for each type of request. This hierarchy is fundamental — it determines which file handles any given URL on your site.

Template loading order (most specific to least specific):

  1. Custom template (assigned per-post in wp-admin)
  2. Type-specific template (e.g. single-{post-type}.php, page-{slug}.php)
  3. Generic template (e.g. single.php, page.php, archive.php)
  4. singular.php or index.php — the universal fallback
URL type Template files checked (in order)
Single post single-{post-type}-{slug}.php → single-{post-type}.php → single.php → singular.php → index.php
Static page custom-template.php → page-{slug}.php → page-{id}.php → page.php → singular.php → index.php
Category archive category-{slug}.php → category-{id}.php → category.php → archive.php → index.php
Author archive author-{nicename}.php → author-{id}.php → author.php → archive.php → index.php
Date archive date.php → archive.php → index.php
Search results search.php → index.php
404 page 404.php → index.php
Front page front-page.php → home.php (if set as blog) → page.php or index.php

Step 3: Build a Classic WordPress Theme

File Structure

your-theme/
  style.css          — required: theme header + base styles
  index.php          — required: fallback template
  functions.php      — feature registration, asset enqueuing
  header.php         — site header partial
  footer.php         — site footer partial
  single.php         — single post template
  page.php           — static page template
  archive.php        — post archive template
  search.php         — search results template
  404.php            — 404 error page
  sidebar.php        — sidebar partial (optional)
  screenshot.png     — theme thumbnail in wp-admin (880×660px)

style.css — The Theme Header

WordPress reads the comment block at the top of style.css to identify and display your theme.

/*
Theme Name: Your Theme Name
Theme URI: https://yourdomain.com
Author: Your Name
Author URI: https://yourdomain.com
Description: A custom WordPress theme built for performance.
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: your-theme
Tags: custom, performance
*/

functions.php — Registering Features and Assets

<?php

function your_theme_setup() {
    // Enable title tag management by WordPress
    add_theme_support( 'title-tag' );

    // Enable featured images on posts and pages
    add_theme_support( 'post-thumbnails' );

    // Enable HTML5 markup for search forms, comment forms, etc.
    add_theme_support( 'html5', array(
        'search-form', 'comment-form', 'comment-list', 'gallery', 'caption',
    ) );

    // Register navigation menus
    register_nav_menus( array(
        'primary' => __( 'Primary Menu', 'your-theme' ),
        'footer'  => __( 'Footer Menu', 'your-theme' ),
    ) );
}
add_action( 'after_setup_theme', 'your_theme_setup' );

// Enqueue styles and scripts
function your_theme_scripts() {
    wp_enqueue_style(
        'your-theme-style',
        get_stylesheet_uri(),
        array(),
        wp_get_theme()->get( 'Version' )
    );

    wp_enqueue_script(
        'your-theme-script',
        get_template_directory_uri() . '/js/main.js',
        array(),
        '1.0.0',
        true  // load in footer
    );
}
add_action( 'wp_enqueue_scripts', 'your_theme_scripts' );

header.php

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo( 'charset' ); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="profile" href="https://gmpg.org/xfn/11">
    <?php wp_head(); ?>
</head>

<body <?php body_class(); ?>>
<?php wp_body_open(); ?>

<header class="site-header">
    <div class="container">
        <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-logo">
            <?php bloginfo( 'name' ); ?>
        </a>
        <nav class="site-navigation" aria-label="Primary">
            <?php
            wp_nav_menu( array(
                'theme_location' => 'primary',
                'menu_class'     => 'nav-menu',
            ) );
            ?>
        </nav>
    </div>
</header>

footer.php

<footer class="site-footer">
    <div class="container">
        <p>&copy; <?php echo date( 'Y' ); ?> <?php bloginfo( 'name' ); ?></p>
        <?php
        wp_nav_menu( array(
            'theme_location' => 'footer',
            'menu_class'     => 'footer-menu',
        ) );
        ?>
    </div>
</footer>

<?php wp_footer(); ?>
</body>
</html>

index.php — The Loop

The Loop is WordPress’s mechanism for querying and displaying posts. Every template that renders post content uses it.

<?php get_header(); ?>

<main id="main" class="site-main">
    <div class="container">

        <?php if ( have_posts() ) : ?>
            <?php while ( have_posts() ) : the_post(); ?>

                <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                    <header class="entry-header">
                        <h2 class="entry-title">
                            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                        </h2>
                        <div class="entry-meta">
                            <span class="posted-on"><?php the_date(); ?></span>
                            <span class="byline"> by <?php the_author(); ?></span>
                        </div>
                    </header>

                    <?php if ( has_post_thumbnail() ) : ?>
                        <div class="post-thumbnail">
                            <?php the_post_thumbnail( 'large' ); ?>
                        </div>
                    <?php endif; ?>

                    <div class="entry-content">
                        <?php the_excerpt(); ?>
                    </div>

                    <a href="<?php the_permalink(); ?>" class="read-more">
                        Read More <span class="screen-reader-text">about <?php the_title(); ?></span>
                    </a>
                </article>

            <?php endwhile; ?>

            <?php the_posts_pagination(); ?>

        <?php else : ?>

            <p>No posts found.</p>

        <?php endif; ?>

    </div>
</main>

<?php get_footer(); ?>

single.php — Single Post Template

<?php get_header(); ?>

<main id="main" class="site-main">
    <div class="container">

        <?php while ( have_posts() ) : the_post(); ?>

            <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>

                <header class="entry-header">
                    <h1 class="entry-title"><?php the_title(); ?></h1>
                    <div class="entry-meta">
                        <span>Published <?php the_date(); ?></span>
                        <span> &middot; <?php the_category( ', ' ); ?></span>
                    </div>
                </header>

                <?php if ( has_post_thumbnail() ) : ?>
                    <div class="post-thumbnail">
                        <?php the_post_thumbnail( 'full', array( 'loading' => 'eager' ) ); ?>
                    </div>
                <?php endif; ?>

                <div class="entry-content">
                    <?php the_content(); ?>
                </div>

                <footer class="entry-footer">
                    <?php the_tags( 'Tags: ', ', ' ); ?>
                </footer>

            </article>

            <?php
            the_post_navigation( array(
                'prev_text' => '&larr; %title',
                'next_text' => '%title &rarr;',
            ) );
            ?>

        <?php endwhile; ?>

    </div>
</main>

<?php get_footer(); ?>

Step 4: Build a Modern Block Theme with theme.json

Block themes replace PHP templates with HTML files containing block markup, and use theme.json as the single source of truth for design tokens, colour palettes, typography, and spacing. The result is a theme that works seamlessly with the Site Editor and requires far less PHP than a classic theme.

For a deeper treatment of performance-focused block theme architecture, see how to build high-performance block themes.

Block Theme File Structure

your-block-theme/
  style.css            — theme header (same as classic)
  functions.php        — minimal: just add_theme_support calls
  theme.json           — design tokens, global styles, block settings
  templates/
    index.html         — fallback template
    single.html        — single post template
    page.html          — page template
    archive.html       — archive template
    404.html           — 404 template
  parts/
    header.html        — header block template part
    footer.html        — footer block template part

theme.json — Design Tokens and Global Settings

{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 3,
  "settings": {
    "color": {
      "palette": [
        { "slug": "primary",    "color": "#2563eb", "name": "Primary" },
        { "slug": "secondary",  "color": "#1e40af", "name": "Secondary" },
        { "slug": "background", "color": "#ffffff", "name": "Background" },
        { "slug": "text",       "color": "#1f2937", "name": "Text" },
        { "slug": "muted",      "color": "#6b7280", "name": "Muted" }
      ]
    },
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
          "slug": "system",
          "name": "System Font"
        }
      ],
      "fontSizes": [
        { "slug": "sm",   "size": "0.875rem", "name": "Small" },
        { "slug": "base", "size": "1rem",      "name": "Base" },
        { "slug": "lg",   "size": "1.125rem", "name": "Large" },
        { "slug": "xl",   "size": "1.25rem",  "name": "XL" },
        { "slug": "2xl",  "size": "1.5rem",   "name": "2XL" },
        { "slug": "3xl",  "size": "1.875rem", "name": "3XL" },
        { "slug": "4xl",  "size": "2.25rem",  "name": "4XL" }
      ]
    },
    "spacing": {
      "spacingSizes": [
        { "slug": "xs",  "size": "0.5rem",  "name": "XS" },
        { "slug": "sm",  "size": "1rem",    "name": "SM" },
        { "slug": "md",  "size": "1.5rem",  "name": "MD" },
        { "slug": "lg",  "size": "2rem",    "name": "LG" },
        { "slug": "xl",  "size": "3rem",    "name": "XL" },
        { "slug": "2xl", "size": "4rem",    "name": "2XL" }
      ]
    },
    "layout": {
      "contentSize": "720px",
      "wideSize": "1200px"
    }
  },
  "styles": {
    "color": {
      "background": "var(--wp--preset--color--background)",
      "text": "var(--wp--preset--color--text)"
    },
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--system)",
      "fontSize": "var(--wp--preset--font-size--base)",
      "lineHeight": "1.6"
    },
    "elements": {
      "link": {
        "color": { "text": "var(--wp--preset--color--primary)" },
        ":hover": { "color": { "text": "var(--wp--preset--color--secondary)" } }
      },
      "h1": { "typography": { "fontSize": "var(--wp--preset--font-size--4xl)", "fontWeight": "700" } },
      "h2": { "typography": { "fontSize": "var(--wp--preset--font-size--3xl)", "fontWeight": "600" } },
      "h3": { "typography": { "fontSize": "var(--wp--preset--font-size--2xl)", "fontWeight": "600" } }
    }
  }
}

templates/index.html

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

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">

    <!-- wp:query {"queryId":1,"query":{"perPage":10,"offset":0,"postType":"post"}} -->
    <div class="wp-block-query">

        <!-- wp:post-template -->
            <!-- wp:post-featured-image {"isLink":true} /-->
            <!-- wp:post-title {"isLink":true,"level":2} /-->
            <!-- wp:post-date /-->
            <!-- wp:post-excerpt {"moreText":"Read More"} /-->
        <!-- /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","tagName":"footer"} /-->

parts/header.html

<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between","flexWrap":"wrap"}} -->
<div class="wp-block-group">
    <!-- wp:site-logo /-->
    <!-- wp:site-title /-->
    <!-- wp:navigation {"layout":{"type":"flex","justifyContent":"right"}} /-->
</div>
<!-- /wp:group -->

Minimal functions.php for a Block Theme

<?php

function your_block_theme_setup() {
    add_theme_support( 'wp-block-styles' );
    add_theme_support( 'editor-styles' );
    add_editor_style( 'style.css' );
}
add_action( 'after_setup_theme', 'your_block_theme_setup' );

Step 5: Add Advanced Functionality

Custom Post Types

<?php

function your_theme_register_post_types() {
    register_post_type( 'portfolio', array(
        'labels'      => array(
            'name'          => 'Portfolio',
            'singular_name' => 'Portfolio Item',
        ),
        'public'       => true,
        'has_archive'  => true,
        'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'show_in_rest' => true,  // enables Gutenberg editor
        'rewrite'      => array( 'slug' => 'portfolio' ),
        'menu_icon'    => 'dashicons-portfolio',
    ) );
}
add_action( 'init', 'your_theme_register_post_types' );

// Register taxonomy
function your_theme_register_taxonomies() {
    register_taxonomy( 'portfolio_category', 'portfolio', array(
        'label'        => 'Portfolio Categories',
        'hierarchical' => true,
        'show_in_rest' => true,
        'rewrite'      => array( 'slug' => 'portfolio-category' ),
    ) );
}
add_action( 'init', 'your_theme_register_taxonomies' );

Custom Fields

Advanced Custom Fields (ACF) is the standard for adding structured data to custom post types. For a comparison of ACF against its alternatives, see ACF Pro vs Metabox vs Pods — the choice matters more than it looks once you have 20+ fields across multiple post types.

Schema Markup

Add JSON-LD structured data to help search engines and AI models understand your content.

<?php

function your_theme_add_schema_markup() {
    if ( is_single() ) {
        global $post;
        $schema = array(
            '@context'  => 'https://schema.org',
            '@type'     => 'Article',
            'headline'  => get_the_title(),
            'datePublished' => get_the_date( 'c' ),
            'dateModified'  => get_the_modified_date( 'c' ),
            'author'    => array(
                '@type' => 'Person',
                'name'  => get_the_author(),
            ),
            'publisher' => array(
                '@type' => 'Organization',
                'name'  => get_bloginfo( 'name' ),
            ),
        );
        echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>';
    }
}
add_action( 'wp_head', 'your_theme_add_schema_markup' );

WooCommerce Support

<?php

function your_theme_woocommerce_support() {
    add_theme_support( 'woocommerce' );
    add_theme_support( 'wc-product-gallery-zoom' );
    add_theme_support( 'wc-product-gallery-lightbox' );
    add_theme_support( 'wc-product-gallery-slider' );
}
add_action( 'after_setup_theme', 'your_theme_woocommerce_support' );

Step 6: Test, Optimise, and Deploy

Browser and Device Testing

  • Chrome, Firefox, Safari, Edge — minimum supported browsers
  • iOS Safari and Android Chrome — these account for the majority of mobile traffic
  • BrowserStack for cross-browser testing without owning every device
  • Chrome DevTools device emulation for rapid responsive checking

Performance Optimisation

Custom themes start lean. Keep them that way:

  • Optimise images: WebP format, loading="lazy" on below-the-fold images, responsive srcset
  • Minify CSS/JS with Webpack, Vite, or Gulp in your build process
  • Reduce HTTP requests by inlining critical CSS and deferring non-essential scripts
  • Implement caching — for a clear breakdown of your options, see the WP Rocket vs W3 Total Cache vs LiteSpeed comparison
  • Target a 90+ mobile score on PageSpeed Insights; the WordPress performance guide covers the full diagnostic process for reaching 100/100

Security Best Practices

Beyond clean HTML, custom themes must follow WordPress output escaping and nonce patterns:

  • Always escape output: esc_html(), esc_attr(), esc_url(), wp_kses_post()
  • Use nonces for any form submissions or AJAX calls
  • Never use $_GET/$_POST directly — validate and sanitize all input
  • Prefix all function names, hooks, and globals to avoid collisions

For a broader view of site-level hardening, the WordPress security guide covers the server, plugin, and user-level vectors.

Code Quality Tools

  • Theme Check plugin — validates against WordPress.org theme standards; useful even for private themes
  • WP_DEBUG — set define( 'WP_DEBUG', true ); in wp-config.php during development
  • PHP_CodeSniffer + WordPress Coding Standards — enforces consistent code style
  • Query Monitor — identifies slow database queries, hook conflicts, and PHP errors in development

Deployment

  • Commit theme files to Git before deploying — never deploy uncommitted changes
  • Test on staging before touching production
  • Upload via SFTP, or use GitHub Actions/DeployBot for automated deploys tied to your main branch
  • Run a full smoke test after deploying — the post-launch checklist covers everything to verify before declaring it done
  • Monitor error logs and Core Web Vitals for 24–48 hours after going live

Best Practices Every WordPress Developer Should Follow

  1. Always use get_template_directory_uri() for asset paths, never hardcode URLs
  2. Enqueue assets via wp_enqueue_scripts, never print raw <script> or <link> tags directly
  3. Escape all output — no exceptions. One unescaped variable is an XSS vector
  4. Use the Template Hierarchy — don’t override everything in index.php; let WordPress route to the right template
  5. Prefix everything — functions, hooks, option names, CSS classes. Unprefixed names conflict with plugins
  6. Add theme support before using itadd_theme_support() must run on the after_setup_theme hook
  7. Use wp_head() and wp_footer() — many plugins require these hooks to inject scripts and styles correctly
  8. Store no secrets in theme files — API keys, credentials, and private config go in wp-config.php or environment variables
  9. Use child themes for modifications — if modifying a third-party parent theme, always use a child so updates don’t overwrite your changes
  10. Test with debug mode enabled — shipping with WP_DEBUG on in development catches notices that silently accumulate technical debt

Common Mistakes to Avoid

  • Forgetting wp_head() / wp_footer(). Plugins that don’t find these hooks simply don’t load — contact forms break, analytics don’t fire, and caching plugins produce warnings.
  • Hardcoding URLs. Using https://yourdomain.com/wp-content/themes/your-theme/style.css breaks every time you migrate environments. Use WordPress template functions.
  • Loading everything globally. Loading your slider JavaScript on every page — including the blog and contact page — adds load time for no reason. Use conditional tags (is_front_page(), is_singular()) to scope assets.
  • Skipping internationalization. Wrapping user-facing strings in __() and _e() is a 5-second habit that makes your theme translatable. Omitting it means a full audit later.
  • Not testing without a menu. Nav menus are optional in WordPress. If the site’s navigation breaks when no menu is assigned to the theme location, that’s a bug.
  • Mixing theme and plugin functionality. Custom post types, taxonomies, and shortcodes belong in a plugin, not the theme — so they survive a theme switch.
  • Ignoring mobile-first CSS. Writing desktop styles and overriding for mobile creates larger stylesheets and more complex specificity chains. Start mobile, layer up.

Custom Theme vs Page Builder: An Honest Comparison

Factor Custom Theme Page Builder (Elementor, Divi)
Build time 4–12 weeks 1–3 weeks
Upfront cost £2,000–12,000+ £500–2,000
LCP (typical) 0.8–1.8s 3.5–6s
CSS/JS payload 50–150KB 400–700KB+
Core Web Vitals Passes consistently Fails without aggressive optimisation
Scalability No ceiling Hits limits at scale
Client editing Gutenberg / Site Editor Visual drag-and-drop
Vendor dependency None Builder must stay installed

If you’re weighing this decision and haven’t already, the Elementor vs Divi vs Bricks comparison is worth reading before committing to any builder route. The broader framework for when custom code wins vs when a builder is the pragmatic call is covered in page builders vs custom code.

When to Hire Instead of Build

Building a custom theme from scratch is the right move when you have the time, the appetite for PHP and CSS debugging, and a project that justifies the investment. It’s the wrong move when a deadline is fixed, the design requirements are standard, or the project budget doesn’t accommodate 40–200 hours of development.

If the scope calls for a specialist, the custom theme development service is built for exactly this — hand-coded, no builder dependencies, optimised for Core Web Vitals. If what you need is modifications to an existing theme rather than building from scratch, the WordPress theme customisation service is the faster path. And if speed is the primary problem on a site that already exists, WordPress speed optimisation addresses that without a full rebuild.

Frequently asked questions

A simple classic theme takes an experienced developer 20–40 hours. A complex theme with custom post types, custom Gutenberg blocks, and WooCommerce integration can take 80–200+ hours. Beginners building their first theme should expect 2–4 weeks, split between learning and building. Block themes typically take less development time once you understand theme.json — you're configuring design tokens rather than writing custom PHP for every style decision.

For a classic theme, yes — at least basic PHP for template tags, The Loop, conditional logic, and functions.php. For a block theme, you can build a functional theme using mostly HTML, CSS, and theme.json without writing any PHP. PHP knowledge unlocks more functionality in both theme types: custom post type registration, custom REST API endpoints, and hook-based customisations all require it.

A child theme inherits all files from a parent theme and overrides specific ones. It's the right approach when you want to modify an existing commercial theme without losing your changes on updates. A custom theme built from scratch has no parent — you control every file, every style decision, every template. Use a child theme for targeted modifications to an existing theme; build from scratch when you need complete control and don't want any of the parent theme's overhead.

Underscores remains a solid foundation for learning classic theme development — it provides the minimal file structure without imposing design opinions. It hasn't been updated for block themes, though. For block theme development, use the Create Block Theme plugin (which generates a starter block theme from your current styles) or study Twenty Twenty-Five, WordPress's default block theme, which is intentionally minimal and well-documented.

The structural advantages are built in: semantic HTML (article, header, nav, main), a single h1 per page, proper heading hierarchy, fast load times, featured image support, and clean URL structure. Add JSON-LD schema markup for Article or Product types via wp_head. Integrate with Rank Math or Yoast for meta tag and sitemap management. Avoid render-blocking scripts and optimise image delivery. Custom themes give SEO plugins a cleaner foundation to build on than most commercial themes do.

Yes, but with an important caveat. If Elementor is doing the layout work, use a minimal theme designed for it (like Hello Elementor) rather than a fully custom theme with its own templates. A richly-featured custom theme and Elementor will often conflict — both want to control the page structure. The cleaner approach is to choose one or the other: Elementor for visual layout management, or a custom theme where a developer controls the templates. See the page builders vs custom code guide for the full decision framework.

Block themes for new projects, in most cases. WordPress's active development is focused on the block editor and Full Site Editing — block themes get new features, classic themes get maintenance. Block themes also perform better by default (theme.json generates only the CSS you define, no legacy Customizer overhead) and let clients manage design through the Site Editor without developer involvement. The main reason to still choose classic: your workflow depends heavily on a page builder like Elementor or Divi, which have limited integration with the Site Editor.

Yes. WordPress is licensed under the GPL, which means any theme you build that extends WordPress must also be GPL-compatible — but it can be sold commercially. You can distribute through ThemeForest, Creative Market, or your own site. Submitting to the WordPress.org theme directory is free but requires passing the Theme Review Team's standards, which is a useful exercise even if you ultimately sell elsewhere.

AK

Written by Ajay Khandal

WordPress Developer — building, fixing and speeding up WordPress sites.

Work with me →