How to Build a WordPress Theme From Scratch

Building a WordPress theme from scratch looks intimidating until you see it broken into the right sequence. Here’s the direct answer: plan your file structure, create a style.css header and a functions.php file, build the template hierarchy starting with header.php, footer.php, and index.php, add page and post templates, enable theme support features, enqueue your scripts and styles correctly, then test everything before launch. That’s the whole arc.
I’ve built custom WordPress themes for client projects for years, and the method below is the same one I use on real builds, not a recycled checklist.
This guide walks through each step in order with copy-paste-ready code, so you can go from an empty folder to a working theme in one sitting. If you already have a Figma file or static HTML mockup you want turned into WordPress, head to the Figma to WordPress conversion guide instead. But if you want to understand WordPress theme architecture at the code level, keep reading.
What You Need Before You Start Building a Theme
You don’t need to be a senior developer to build your first WordPress theme, but you do need specific baseline skills and setup. Skipping this step is the number one reason first-time theme builds stall out halfway through.
Technical prerequisites
A WordPress theme is fundamentally a folder of PHP templates styled with CSS and enhanced with JavaScript. You should be comfortable with:
- HTML: structuring markup semantically (headers, nav, articles, footers)
- CSS: styling layout, typography, and responsiveness
- Basic PHP: reading template tags, conditionals, and loops (you don’t need to be a PHP expert, but you need to read it without hesitation)
- JavaScript basics: helpful if you plan to enqueue custom scripts or interactivity
If PHP is the gap, focus your first learning hours there. HTML and CSS transfer directly; PHP is what’s genuinely new for most designers moving into theme development.
Local development environment setup
Never build a theme directly on a live server. Set up a local WordPress install using a tool like Local by WP Engine or a Docker-based stack, install a fresh copy of WordPress, and activate a blank starter theme so you have a clean slate. This lets you break things safely, which you will, especially the first time you touch functions.php.
Classic theme vs. block theme: which to build
WordPress supports two theme architectures. Choosing the right one before you write a single file saves hours of rework later.
| Feature | Classic Theme | Block Theme |
|---|---|---|
| Templates written in | PHP (header.php, footer.php, index.php) | HTML with block markup, stored in /templates |
| Styling control | style.css and functions.php | theme.json and style.css |
| Site Editor support | Limited or none | Full Site Editor (Full Site Editing) support |
| Learning curve | Familiar if you know PHP | Requires learning theme.json schema and block templates |
| Best for | Developers who want granular PHP control | Sites built primarily with the block editor |
The WordPress Developer Handbook covers both architectures in detail. This guide focuses on the classic theme approach, since it’s still the most common entry point for developers learning theme structure and makes the template hierarchy easiest to understand.
Prerequisites Checklist
- Comfortable reading basic PHP syntax and conditionals
- Local WordPress install running (not a live production site)
- Code editor installed (VS Code, Sublime Text, or similar)
- Decided: classic theme or block theme
- Backup of any existing site before testing on it
Step 1: Plan the Theme’s File and Folder Structure
Every WordPress theme lives in its own folder inside /wp-content/themes/. WordPress only requires two files to recognize a folder as a valid theme: style.css and index.php. Everything else is optional but expected in any real-world build.
| File | Required? | Purpose |
|---|---|---|
| style.css | Yes | Theme metadata and main stylesheet |
| index.php | Yes | Fallback template for all content |
| functions.php | Strongly recommended | Theme setup, hooks, enqueueing |
| header.php | Recommended | Site header, opens <html> and <head> |
| footer.php | Recommended | Site footer, closes tags |
| screenshot.png | Recommended | Preview image in the WordPress admin |
| single.php | Optional | Template for individual blog posts |
| page.php | Optional | Template for static pages |
| archive.php | Optional | Template for post archives |
| 404.php | Optional | Custom "not found" page |
Recommended folder organization
A clean, scalable folder structure looks like this:
my-custom-theme/
├── style.css
├── index.php
├── functions.php
├── header.php
├── footer.php
├── page.php
├── single.php
├── archive.php
├── 404.php
├── screenshot.png
├── /assets
│ ├── /css
│ ├── /js
│ └── /images
├── /template-parts
│ ├── content.php
│ └── content-page.php
└── /inc
├── enqueue.php
└── theme-setup.php
The /assets folder keeps your compiled CSS and JS separate from PHP logic. The /template-parts folder holds reusable chunks you pull into templates with get_template_part(), which keeps individual template files short and readable. The /inc folder is where I split functions.php logic into smaller files once a theme grows past a few hundred lines, since one giant functions.php becomes unmanageable fast on real client builds.
Step 2: Set Up style.css and functions.php
These two files are the backbone of every classic WordPress theme. Get them wrong and nothing else works.
Writing the style.css header comment block
WordPress reads a specific comment block at the top of style.css to identify your theme in the admin dashboard. Without it, WordPress won’t recognize the folder as a theme.
/*
Theme Name: My Custom Theme
Theme URI: https://example.com/my-custom-theme
Author: Your Name
Author URI: https://example.com
Description: A custom-built WordPress theme for learning the fundamentals of theme development.
Version: 1.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-custom-theme
*/
Everything below this comment block is standard CSS. WordPress parses these header fields to display the theme name, description, and version in Appearance > Themes.
Creating a minimal functions.php
functions.php acts like a plugin that’s built into your theme. It’s where you hook into WordPress core to add features, register menus, and load your assets. Write it as pure PHP, not wrapped in closing and opening tags.
<?php
// Theme setup
function my_theme_setup() {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'custom-logo' );
register_nav_menus( array(
'primary' => __( 'Primary Menu', 'my-custom-theme' ),
) );
}
add_action( 'after_setup_theme', 'my_theme_setup' );
That’s a working, minimal functions.php. From here you’ll add enqueueing (Step 6) and any custom logic your site needs.
Step 3: Build the Template Hierarchy (header, footer, index)
The template hierarchy is WordPress’s internal decision tree for choosing which PHP file renders any given request. Understanding it is the single most important concept in classic theme development.
How WordPress chooses which template to load
When a visitor requests a single blog post, WordPress checks in this order:
single-{post-type}-{slug}.phpsingle-{post-type}.phpsingle.phpsingular.phpindex.php(final fallback, always present)
The same logic applies to pages, archives, categories, tags, search results, and 404s, each with its own hierarchy chain that always terminates at index.php. The official Theme Handbook’s template hierarchy documentation has the full diagram for every content type and is worth bookmarking as a reference while you build.
Creating header.php and footer.php
header.php opens the HTML document and typically includes the <head> section, site branding, and navigation:
<!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(); ?>>
footer.php closes what header.php opened and always calls wp_footer(), where WordPress and plugins hook in scripts:
<?php wp_footer(); ?>
</body>
</html>
Never skip wp_head() or wp_footer(). Plugins depend on these hooks firing, and a large share of "my plugin isn’t working" support tickets trace back to a theme missing one of these calls.
Creating index.php as the fallback template
index.php is the only template file WordPress absolutely requires beyond style.css, and it’s the final fallback in every hierarchy chain. A basic version pulls in your header, runs the Loop, and pulls in your footer:
<?php get_header(); ?>
<main id="main-content">
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<h2><?php the_title(); ?></h2>
<div><?php the_content(); ?></div>
</article>
<?php endwhile; endif; ?>
</main>
<?php get_footer(); ?>
have_posts() and the_post() together form what WordPress calls "the Loop," the mechanism every template uses to fetch and display content from the database.
Watching someone build this out live helps it click faster than reading code alone. LearnWebCode’s walkthrough covers this exact process from a blank folder to a working theme:
Step 4: Create Page and Post Templates
Once your fallback templates work, you can build more specific templates for pages and posts so different content types can look and behave differently.
Custom page templates
A custom page template is a PHP file with a special comment at the top that makes it selectable from the Page Attributes panel in the editor:
<?php
/* Template Name: Full Width Page */
get_header();
?>
<main class="full-width">
<?php while ( have_posts() ) : the_post(); the_content(); endwhile; ?>
</main>
<?php get_footer(); ?>
Save this as page-full-width.php and it appears as "Full Width Page" in the template dropdown for any page in the admin.
single.php and archive.php for posts
single.php controls how one individual blog post displays. archive.php controls how a list of posts (by category, tag, date, or author) displays. Both are applied automatically by the template hierarchy, unlike page templates which editors select by hand.
| Aspect | Page Templates | Post Templates (single.php, archive.php) |
|---|---|---|
| Assignment | Manually selected per page in the editor | Automatically applied by the template hierarchy |
| Naming convention | page-{slug}.php or custom-named with a Template Name comment |
single.php, single-{post-type}.php, archive.php |
| Typical use case | Landing pages, contact pages, unique layouts | Blog posts, custom post type archives |
| Reusability | One template can serve many different pages | Applies globally to all posts of that type unless overridden |
Step 5: Add Theme Support Features and Custom Functions
WordPress core ships with a long list of features that themes must explicitly opt into. This is where add_theme_support() comes in.
Using add_theme_support (post thumbnails, menus, title-tag)
Each call tells WordPress "yes, this theme supports this feature," which unlocks admin UI and template functions that would otherwise be unavailable.
Common add_theme_support Features
title-tag– lets WordPress manage the<title>tag automaticallypost-thumbnails– enables featured imagescustom-logo– enables the Site Identity logo uploaderhtml5– modernizes markup for search forms, comments, galleriescustom-background– enables a background color or image customizer optionautomatic-feed-links– adds RSS feed links to the headalign-wide– allows wide and full-width block alignment (useful even in classic themes using the block editor for content)
Registering navigation menus and widget areas
Menus and widget areas (sidebars) need registration in functions.php before they can be assigned in the admin:
function my_theme_widgets_init() {
register_sidebar( array(
'name' => __( 'Main Sidebar', 'my-custom-theme' ),
'id' => 'sidebar-1',
'before_widget' => '<div class="widget">',
'after_widget' => '</div>',
'before_title' => '<h3>',
'after_title' => '</h3>',
) );
}
add_action( 'widgets_init', 'my_theme_widgets_init' );
Once registered, both menus and widget areas show up automatically under Appearance in the dashboard, ready for a site owner to populate without touching code.
Step 6: Enqueue Styles and Scripts Properly
This step trips up more first-time theme builders than any other. It’s also the one client project I most often get called in to fix, usually a theme where every stylesheet was hardcoded straight into header.php.
Why not to hardcode <link>/<script> tags
Hardcoding a <link rel="stylesheet"> or <script src=""> tag directly into header.php bypasses WordPress’s dependency management system entirely. That means no version control for cache-busting, no way for plugins to detect and avoid loading duplicate libraries like jQuery, and no clean way to conditionally load scripts only where they’re needed. It works in a demo. It causes conflicts on a real site with plugins installed.
Using wp_enqueue_style and wp_enqueue_script correctly
Both functions must run inside a function hooked to wp_enqueue_scripts, never called directly in the global scope of a template file:
function my_theme_scripts() {
wp_enqueue_style(
'my-theme-style',
get_stylesheet_uri(),
array(),
'1.0'
);
wp_enqueue_script(
'my-theme-script',
get_template_directory_uri() . '/assets/js/main.js',
array(),
'1.0',
true
);
}
add_action( 'wp_enqueue_scripts', 'my_theme_scripts' );
The fourth parameter in wp_enqueue_script() is the version number (helps with cache busting after updates), and the fifth (true) loads the script in the footer rather than the head, which is best practice for performance since it doesn’t block page rendering.
On one client rebuild, the previous developer had hardcoded four <script> tags directly in header.php, including a duplicate copy of jQuery already bundled with WordPress core. It silently broke a contact form plugin because two versions of jQuery were fighting for the same namespace. Switching everything to properly enqueued, deduplicated assets fixed it in under ten minutes. Enqueueing isn’t a formality, it’s what keeps your theme compatible with the plugin ecosystem.
Step 7: Test, Debug, and Prepare for Launch
A theme that "looks right" on your screen isn’t the same as a theme that’s ready to launch. This step is where amateur builds and production-ready builds diverge.
Enabling WP_DEBUG and checking for errors
Add this to your local wp-config.php (never on a live production site) to surface hidden PHP notices and warnings:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
This logs errors to /wp-content/debug.log instead of displaying them on screen, so you can review issues without exposing them to site visitors.
Cross-browser and responsiveness testing
Check your theme in at least Chrome, Firefox, and Safari, plus a real mobile device if you can, not just a browser’s device-emulation mode. Test:
- Navigation menu behavior on mobile (does it collapse correctly?)
- Featured images and post thumbnails at different screen widths
- Widget areas and footer columns stacking properly on small screens
- Forms and interactive JavaScript elements functioning without console errors
Common first-theme problems and fixes
| Problem | Likely Cause | Fix |
|---|---|---|
| Blank white screen | PHP fatal error, often a missing semicolon or unclosed bracket | Enable WP_DEBUG and check debug.log |
| Theme not appearing in Appearance > Themes | Missing or malformed style.css header comment | Verify the comment block syntax exactly matches the required format |
| Styles not loading | CSS hardcoded instead of enqueued, or wrong file path in wp_enqueue_style | Confirm the path with get_stylesheet_uri() or get_template_directory_uri() |
| Menu not showing | Menu registered in functions.php but not assigned to a location in Appearance > Menus | Assign the menu location in the admin |
| Featured image not displaying | Missing add_theme_support(‘post-thumbnails’) or missing the_post_thumbnail() call in the template | Add both the theme support call and the template function |
Pre-Launch QA Checklist
- WP_DEBUG enabled and debug.log reviewed, no unresolved notices
- All templates tested: home, single post, page, archive, search, 404
- Menus and widget areas assigned and displaying correctly
- All scripts and styles loading via enqueue, none hardcoded
- Tested on at least two browsers and one real mobile device
- screenshot.png added for the admin theme preview
- Text domain consistent across all translatable strings
To see a fuller theme built end to end with more advanced patterns, freeCodeCamp’s full-length course walks through an entire custom build:
Should You Build a Theme From Scratch or Hire a Developer?
Building your own theme is a genuinely good way to learn WordPress’s architecture and gives you total control over markup, performance, and functionality. It also takes real time, and every hour spent debugging enqueue conflicts is an hour not spent on the actual business the site supports.
Build it yourself if: you already know PHP fundamentals, you want to learn theme architecture deeply, the site is a portfolio or learning project, and you have time to troubleshoot.
Use a starter theme if: you want a head start on boilerplate (menu registration, basic enqueueing, accessibility markup already handled) without giving up customization control. Underscores (_s) from Automattic is a well-known example of this approach.
Hire a developer if: the site is client-facing or revenue-generating, you need it done reliably on a timeline, or the design requirements go beyond what you’re confident building and maintaining yourself.
If your business needs a theme that reflects real design intent rather than a generic template, it’s worth reading what actually defines a custom WordPress theme before deciding, and comparing custom themes against off-the-shelf templates for your specific situation. For teams that would rather hand this off entirely, custom WordPress theme development work at Ivory Miracle picks up exactly where this tutorial leaves off, building a production-grade theme around your actual content and brand rather than a generic starter. And if you’re not building the theme yourself but need someone who is, this guide to hiring a freelance WordPress developer walks through vetting and scoping the work.
Frequently Asked Questions
Can ChatGPT build a WordPress website?
ChatGPT and similar tools can generate PHP snippets, suggest functions.php code, and help debug errors, but they can’t independently create, upload, activate, and configure a working theme inside your WordPress install. Treat them as a coding assistant for individual files, not a replacement for understanding the template hierarchy yourself.
Which theme builder is best for WordPress?
This depends on your goals. If you want full code control, building a classic or block theme from scratch gives you the most flexibility. If you’d rather use a visual page builder, Gutenberg’s Site Editor for block themes is the option built directly into WordPress core, per the WordPress Developer Handbook.
What is the easiest WordPress theme to use for beginners?
For non-developers who don’t want to write any code, a well-supported block theme configured through the Site Editor is generally the most approachable path, since it uses a visual interface rather than PHP templates.
Are WordPress themes free?
Many are. The WordPress.org theme directory hosts thousands of free, reviewed themes that meet WordPress’s Theme Review Guidelines. Premium themes with additional features or support are sold separately by third-party developers and marketplaces.
What files are required to build a WordPress theme?
Only two files are technically required: style.css with a valid theme header comment block, and index.php as the fallback template. Every other file, including functions.php, header.php, and footer.php, is optional but expected in any real-world theme.
What is the WordPress template hierarchy?
The WordPress template hierarchy is the ordered set of rules WordPress follows to decide which template file renders a given page, checking specific file names first (like single-post.php) and falling back to more general ones (like index.php) if a more specific match doesn’t exist.
Do I need to know PHP to build a WordPress theme?
For a classic theme, yes, at least at a basic level, since templates are written in PHP mixed with HTML. Block themes reduce the PHP requirement significantly by relying on theme.json and block markup instead, making them more approachable if PHP is a hard blocker for you.
How long does it take to build a WordPress theme from scratch?
A simple classic theme with the core templates covered in this guide takes a few days to two weeks for someone comfortable with PHP and CSS, working part-time. More complex themes with custom post types, multiple page templates, and full accessibility and performance testing take longer.
What is a child theme and when should I use one?
A child theme inherits all the functionality and styling of a parent theme while letting you override specific templates or styles in its own folder, without editing the parent theme’s files directly. Use one whenever you’re customizing an existing theme (yours or a third-party one) so your changes survive parent theme updates.
Can I build a WordPress theme without coding?
Yes, through block themes and the Site Editor, which let you assemble templates visually using blocks and patterns rather than writing PHP. It’s a fundamentally different workflow from the classic theme process in this guide, and it trades some code-level control for accessibility to non-developers.
Getting Started
Building a WordPress theme from scratch is one of those skills that feels overwhelming right up until the moment your first index.php successfully renders a post. After that, the template hierarchy stops being an abstract diagram and becomes something you can predict and control. Start with the file structure, get style.css and functions.php right, and build outward one template at a time. That’s the whole method, and it scales from a weekend learning project to a full client theme without changing the fundamentals.
Sources
- Theme Handbook, Developer.WordPress.org
- Template Hierarchy, WordPress Developer Handbook
- WordPress.org Theme Directory
- Local by WP Engine
Related: custom WordPress theme development
My right hand does not know how much my left hand has given, and vice versa. Over the years, I have given to orphanages, street children and their families, the elderly, women seeking livelihood opportunities, widows, and people who are on the fringes of society. My logic is simple: I never want anyone to feel as though they have been forgotten by society. Sometimes it does not take much. People on the streets have actually smiled at me simply because I gave them a cookie. That smile can stay with you much longer than you expect. No one should feel forgotten. I’m a little like the city that raised me. My favorite food is beef pares, tenderly cooked beef simmered for several hours until the meat practically melts in your mouth. Its taste cannot easily be contained by a poverty of vocabulary when it is cooked just right, and the stew is simply the icing on the cake. In a strange way, it reminds me of the beautiful ways God has arranged my heart through my unique experiences in life. Manila itself feels like a melting pot, no pun intended, of food, beverages, languages, art, people, contradictions, and real life. I think I have become a little like the city that raised me. Read About Me.



2 comments
Good work gets better when people bring thoughtful questions, ideas, and perspective.