Welcome to the new GPLDL Site - all systems operational!

GPLDL
Search
Open menu
Tutorials

How to Write Your Own WordPress Theme From Scratch

A modern guide to building a custom WordPress theme from scratch, covering planning, block theme structure, theme.json, templates, assets, testing, and maintenance.

13 min read

GPLDL tutorial illustration

Creating your own WordPress theme is still one of the best ways to understand how WordPress works. A theme is not just a skin. It is the layer that decides how posts, pages, archives, search results, navigation, typography, spacing, and media are presented to visitors.

The basic idea is simple: WordPress stores the content, your theme displays it. The hard part is building the theme in a way that remains maintainable after the first version is finished.

This guide walks through a practical, modern approach to building a WordPress theme from scratch. It focuses on block themes because they are the current direction of WordPress theme development, while also explaining when a classic theme still makes sense.

Start With the Right Theme Type

Before creating files, decide what kind of theme you are building.

Block themes are the best starting point for most new projects. They use HTML-based block templates, template parts, patterns, and theme.json. Site owners can edit templates and global styles in the Site Editor, while developers can still version-control the theme files.

Classic themes use PHP template files such as index.php, single.php, page.php, and archive.php. They are still supported and widely used, especially on older projects, heavily customized sites, or builds that depend on a traditional PHP workflow.

Hybrid themes are classic themes that adopt selected block-era features such as theme.json, block styles, or block template parts. They are useful when you need to modernize an existing classic theme without rebuilding everything at once.

For a new theme, choose a block theme unless you have a clear reason not to. If you are maintaining an older site, a classic or hybrid theme may be the more realistic choice.

What Belongs in a Theme

A good theme controls presentation. It should define layout, typography, colors, spacing, template structure, image treatment, and editor-facing design rules.

It should not hold business-critical functionality that must survive a theme change. Custom post types, checkout logic, member permissions, analytics integrations, and complex forms usually belong in plugins. If a site would break when the theme is switched, that feature probably should not live only in the theme.

Use this rule of thumb:

  • Put design and front-end presentation in the theme.
  • Put durable site functionality in plugins.
  • Put reusable content sections in patterns.
  • Put global design decisions in theme.json.
  • Put only small theme-specific PHP helpers in functions.php.

That separation keeps the theme easier to replace, audit, and maintain.

Set Up a Local Development Environment

Do not build or test a theme directly on a live website. Use a local WordPress installation so you can break things, inspect errors, and test edge cases without affecting visitors.

Common local development options include Local, XAMPP, MAMP, Docker-based stacks, Laravel Herd, DDEV, and WordPress Playground. The tool matters less than having a repeatable environment with WordPress, PHP, a database, and access to the theme files.

At minimum, prepare:

  • A fresh WordPress installation.
  • A code editor with PHP, HTML, CSS, and JSON support.
  • Version control, preferably Git.
  • Test content with posts, pages, images, galleries, quotes, lists, comments, and long titles.
  • Debugging enabled during development.

For PHP debugging, add this to your local wp-config.php file:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );

Keep those settings out of production unless you know exactly how your host handles error display and logs.

Plan the Theme Before Writing Code

The fastest way to create a messy theme is to start coding before you know what the theme needs to support.

Write down the core requirements first:

  • What content types will the site publish?
  • Does the homepage need a custom layout?
  • Which archive pages matter?
  • Will editors need reusable sections?
  • Which colors, fonts, and spacing values should be locked down?
  • Which design choices should editors be allowed to change?
  • Does the theme need custom JavaScript?
  • Does it need to support comments, search, WooCommerce, memberships, or multilingual content?

Then sketch the template list. A simple site may only need an index template, a page template, a single post template, an archive template, a search template, and a 404 template. A larger site may need custom templates for product pages, documentation, landing pages, or editorial features.

Create the Theme Folder

In your local WordPress installation, create a new folder inside:

wp-content/themes/

Use a lowercase, descriptive folder name with hyphens:

wp-content/themes/acme-starter/

For a modern block theme, start with this structure:

acme-starter/
style.css
theme.json
functions.php
screenshot.png
templates/
index.html
front-page.html
home.html
single.html
page.html
archive.html
search.html
404.html
parts/
header.html
footer.html
patterns/
hero.php
call-to-action.php
styles/
dark.json
assets/
css/
js/
images/

Only two files are required for WordPress to recognize a block theme:

  • style.css
  • templates/index.html

Everything else is optional, but most real themes will need more structure.

Add the Theme Header in style.css

Every WordPress theme needs a style.css file in the root of the theme folder. In block themes, this file is often used mostly for theme metadata and small fallback styles, while most design rules live in theme.json and block styles.

Create style.css:

/*
Theme Name: Acme Starter
Theme URI: https://example.com/acme-starter
Author: Your Name
Author URI: https://example.com
Description: A custom block theme built from scratch.
Version: 1.0.0
Requires at least: 6.6
Tested up to: 6.9
Requires PHP: 8.1
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: acme-starter
Tags: blog, portfolio, full-site-editing, block-patterns, editor-style
*/

The header is how WordPress identifies the theme in Appearance > Themes. If you plan to distribute the theme publicly, keep the metadata accurate and replace the compatibility values with versions you have actually tested. The Version value is especially important because update systems use it to determine whether a newer release exists.

Define Global Settings With theme.json

The theme.json file is central to modern WordPress theme development. It defines editor controls, color palettes, typography presets, spacing rules, layout widths, block styles, and global front-end styles.

If your minimum supported WordPress version is 6.6 or newer, use theme.json version 3:

{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "720px",
"wideSize": "1120px"
},
"color": {
"defaultPalette": false,
"palette": [
{
"name": "Foreground",
"slug": "foreground",
"color": "#171717"
},
{
"name": "Background",
"slug": "background",
"color": "#ffffff"
},
{
"name": "Accent",
"slug": "accent",
"color": "#2563eb"
}
]
},
"typography": {
"defaultFontSizes": false,
"fontSizes": [
{
"name": "Small",
"slug": "small",
"size": "0.875rem"
},
{
"name": "Medium",
"slug": "medium",
"size": "1rem"
},
{
"name": "Large",
"slug": "large",
"size": "1.5rem"
}
]
}
},
"styles": {
"color": {
"background": "var:preset|color|background",
"text": "var:preset|color|foreground"
},
"typography": {
"fontFamily": "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif",
"lineHeight": "1.65"
},
"elements": {
"link": {
"color": {
"text": "var:preset|color|accent"
}
}
}
}
}

This does several useful things:

  • It gives your theme a controlled color palette.
  • It disables default font sizes so your design system is clearer.
  • It sets content and wide layout widths.
  • It makes typography and link styling consistent across the front end and editor.
  • It gives editors useful controls without leaving every visual decision open.

Treat theme.json as your design contract. If a color, spacing value, or font size is part of the system, define it there instead of scattering hard-coded values across templates.

Build the First Template

A block theme needs a fallback template at:

templates/index.html

A minimal version might look like this:

<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:query {"query":{"perPage":10,"postType":"post","order":"desc","orderBy":"date"}} -->
<div class="wp-block-query">
<!-- wp:post-template -->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- wp:post-date /-->
<!-- /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"} /-->

Block templates are HTML files that contain WordPress block markup. You can write them by hand, but many developers build a layout in the Site Editor, copy the block markup, then commit the cleaned-up version to the theme.

Template parts are reusable pieces of layout. A header and footer are the obvious first examples.

Create parts/header.html:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:site-title /-->
<!-- wp:navigation /-->
</div>
<!-- /wp:group -->

Create parts/footer.html:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:paragraph -->
<p>Copyright 2026</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

In a production theme, the footer should usually use dynamic blocks, navigation areas, patterns, or editable content instead of hard-coded text. This minimal example is only a starting point.

Understand the Template Hierarchy

WordPress decides which template to use through the template hierarchy. The more specific template wins. If WordPress cannot find a specific match, it falls back until it reaches the required index template.

For example, a single page may use this order in a block theme:

custom template
page-about.html
page-42.html
page.html
index.html

A single post may use:

single-post.html
single.html
singular.html
index.html

An archive may use:

category-news.html
category.html
archive.html
index.html

You do not need to create every possible template. Create the ones your design actually needs. The fallback system is there so the theme can remain small.

Register Theme Features and Assets

Block themes often need less PHP than classic themes, but functions.php is still useful for theme setup, custom assets, block styles, and editor support.

Create functions.php:

<?php
/**
* Theme setup.
*/
function acme_starter_setup() {
add_editor_style( 'style.css' );
}
add_action( 'after_setup_theme', 'acme_starter_setup' );
/**
* Enqueue front-end assets.
*/
function acme_starter_enqueue_assets() {
wp_enqueue_style(
'acme-starter-style',
get_stylesheet_uri(),
array(),
wp_get_theme()->get( 'Version' )
);
wp_enqueue_script(
'acme-starter-navigation',
get_theme_file_uri( 'assets/js/navigation.js' ),
array(),
wp_get_theme()->get( 'Version' ),
true
);
}
add_action( 'wp_enqueue_scripts', 'acme_starter_enqueue_assets' );

Use WordPress enqueue functions instead of hard-coding <link> or <script> tags. Enqueueing lets WordPress, plugins, child themes, and dependency management work together properly.

Also prefix function names, handles, classes, and custom identifiers with your theme slug. This reduces conflicts with plugins and other themes.

Create Patterns for Reusable Sections

Patterns are reusable block compositions. They are ideal for sections like hero areas, pricing rows, testimonial groups, feature lists, author boxes, and calls to action.

Create patterns/hero.php:

<?php
/**
* Title: Hero
* Slug: acme-starter/hero
* Categories: featured
*/
?>
<!-- wp:group {"align":"wide","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignwide">
<!-- wp:heading {"level":1} -->
<h1>Build faster with a focused WordPress theme.</h1>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>A clean starting point for publishing, editing, and growing a WordPress site.</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

Patterns make the editing experience better because users can insert a finished section instead of assembling every block from scratch. They also reduce the temptation to hard-code one-off sections into templates.

Style Blocks Carefully

A custom theme should look good on both the front end and inside the editor. If the editor and public site look completely different, content creators cannot trust what they are building.

Prefer this order for styling:

  1. Use theme.json for global design tokens and block-level defaults.
  2. Use block styles or per-block CSS for specific block refinements.
  3. Use style.css for small global corrections.
  4. Use custom CSS files only when the theme genuinely needs more structure.

Avoid overloading the theme with a large CSS framework unless the project requires it. WordPress already provides many layout and spacing tools through blocks and theme.json. A lean theme is easier to debug and usually faster to load.

Handle Dynamic Data Securely

Block templates reduce the amount of PHP you need, but many themes still output dynamic values in PHP files such as patterns, template helpers, or classic templates.

When outputting dynamic data, escape it for the correct context:

<a href="<?php echo esc_url( get_permalink() ); ?>">
<?php echo esc_html( get_the_title() ); ?>
</a>

Use:

  • esc_html() for plain text.
  • esc_attr() for attribute values.
  • esc_url() for URLs.
  • wp_kses_post() when limited HTML is allowed.
  • Nonces for form actions.
  • $wpdb->prepare() for custom database queries.

Do not trust theme options, query parameters, post meta, user input, or remote data. Validate on input, sanitize before saving, and escape on output.

Test With Real Content

A theme that looks good with one perfect demo post is not finished. Test it with awkward content:

  • Very long post titles.
  • Posts with no featured image.
  • Large images and small images.
  • Galleries, embeds, quotes, tables, buttons, and lists.
  • Nested comments, if comments are supported.
  • Empty search results.
  • Long menus.
  • Multiple screen sizes.
  • Different editor roles.
  • Dark style variations, if included.

Import WordPress Theme Unit Test data or create your own stress-test content. Then browse every important template on desktop and mobile.

Use these checks before release:

  • No PHP notices or warnings in debug logs.
  • No missing assets in the browser console.
  • No JavaScript errors.
  • Valid HTML structure.
  • Keyboard-accessible navigation.
  • Sufficient color contrast.
  • Responsive layouts at small, medium, and large widths.
  • Editor styles that match the front end closely enough for content work.
  • Theme Check results reviewed and fixed where relevant.

Testing is where many first themes fail. Do not treat it as a final polish step. Test throughout development.

Package the Theme

When the theme is ready, remove anything that should not ship:

  • Local build caches.
  • Source maps, unless intentionally included.
  • Debug files.
  • Temporary screenshots.
  • Unused assets.
  • Development-only dependencies.
  • TODO comments that no longer help.

Then create a zip file containing the theme folder contents. The zip should install cleanly through Appearance > Themes > Add New > Upload Theme.

If you plan to submit the theme to the WordPress.org Theme Directory, review the current theme requirements before packaging. Directory themes need to follow specific licensing, accessibility, security, screenshot, file, and review rules.

Maintain the Theme After Launch

A custom theme is not done when it launches. WordPress, PHP, browsers, accessibility expectations, and editor features continue to change.

Create a maintenance routine:

  • Test the theme against new WordPress releases.
  • Keep the Tested up to value current only after testing.
  • Review deprecated function notices.
  • Recheck templates after major block editor changes.
  • Keep third-party assets updated.
  • Audit editor permissions and theme options.
  • Document changes in a changelog.
  • Bump the theme version for every release.

If the theme is used on client sites or distributed publicly, maintenance is part of the product. A theme that is not maintained eventually becomes a risk.

A Practical First Build Plan

If you are building your first theme, keep version 1 small:

  1. Create the theme folder.
  2. Add style.css, theme.json, and templates/index.html.
  3. Add header and footer template parts.
  4. Build single.html, page.html, archive.html, search.html, and 404.html.
  5. Define colors, typography, spacing, and layout widths in theme.json.
  6. Add only the CSS and JavaScript you actually need.
  7. Create patterns for repeated sections.
  8. Test with real content and mobile viewports.
  9. Run Theme Check and fix serious issues.
  10. Package the theme and install it on a clean WordPress site.

That path gives you a real working theme without forcing you to understand every advanced WordPress API on day one.

Useful Official References

WordPress theme development changes over time, so keep the official documentation close:

Final Thoughts

Writing a WordPress theme from scratch is less about memorizing file names and more about understanding responsibilities. The theme should present content clearly, give editors a reliable experience, follow WordPress conventions, and avoid owning functionality that belongs elsewhere.

Start with a small block theme, define your design system in theme.json, use templates and patterns intentionally, enqueue assets properly, and test with imperfect real-world content. That foundation will take you much further than copying a large starter theme you do not understand.