A custom WordPress theme means building the presentation layer yourself rather than adapting somebody else’s. In 2026 that decision starts with a second one: whether to build a block theme, a classic theme, or a hybrid of the two. The answer changes which files you need, where your styling lives and how much control your editors get. This guide covers both routes, the files each requires, the template hierarchy, and the security and testing practices that separate a theme you can maintain from one you will rewrite.
Which kind of theme should you build?
WordPress supports two theme architectures, and they are not interchangeable.
Block themes define templates as HTML files composed of blocks, with styling centralised in theme.json. Editors can change layout and templates through the Site Editor without touching code. This is the direction of the platform, and the default themes have been block themes since Twenty Twenty-Two.
Classic themes define templates as PHP files and style them with CSS. They give you precise control over what loads and follow a hierarchy most WordPress developers already know. They remain a valid choice, particularly where you want to restrict what editors can change.
Hybrid themes are classic themes that adopt theme.json for design tokens without moving templates into the Site Editor. In practice this is a common and pragmatic position: you get a design system the block editor respects, while keeping PHP templates.
Pick block themes when editors need genuine layout control and the design is expressible in blocks. Pick classic or hybrid when templates carry heavy custom logic, when you need tight control over what is loaded, or when the client explicitly wants a locked-down editing experience.
What files does a theme actually require?
The minimum depends on which architecture you chose.
| Theme type | Required | Strongly recommended |
|---|---|---|
| Block theme | style.css, templates/index.html | theme.json, parts/header.html, parts/footer.html, functions.php |
| Classic theme | style.css, index.php | functions.php, header.php, footer.php, theme.json |
style.css is required in both cases, because its header comment is how WordPress identifies the theme:
css
/*
Theme Name: Acme
Theme URI: https://example.com/acme
Author: Acme
Description: Custom theme for Acme.
Version: 1.0.0
Requires at least: 6.6
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: acme
*/
Without that header, WordPress will not list the folder as a theme.
A block theme typically looks like this:
acme/
style.css
theme.json
functions.php
templates/
index.html
single.html
page.html
archive.html
404.html
parts/
header.html
footer.html
patterns/
hero.php
styles/
dark.json
A classic theme keeps its templates as PHP in the theme root: index.php, single.php, page.php, archive.php, search.php, 404.php, plus header.php and footer.php as includes.
How do you set up functions.php correctly?
functions.php behaves like a plugin bundled with your theme. Two conventions cause more confusion than anything else in theme development, so state them plainly.
The opening <?php tag is required. The closing ?> tag should be omitted, because trailing whitespace after it can produce output before headers are sent and break the site in ways that are annoying to diagnose.
php
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'after_setup_theme', 'acme_setup' );
function acme_setup() {
add_theme_support( 'post-thumbnails' );
add_theme_support( 'title-tag' );
add_theme_support( 'html5', array( 'search-form', 'comment-form', 'gallery', 'caption' ) );
load_theme_textdomain( 'acme', get_template_directory() . '/languages' );
}
add_action( 'wp_enqueue_scripts', 'acme_assets' );
function acme_assets() {
$stylesheet = get_stylesheet_directory() . '/style.css';
wp_enqueue_style(
'acme-style',
get_stylesheet_uri(),
array(),
file_exists( $stylesheet ) ? filemtime( $stylesheet ) : '1.0.0'
);
}
Two details worth keeping. Prefix every function name, because the global namespace is shared with plugins and core. And version assets with filemtime() rather than a hardcoded string, so browsers pick up changes without a manual version bump.
Never link stylesheets or scripts directly in template markup. Enqueueing lets WordPress and caching layers manage dependencies and order.
How does theme.json work?
theme.json is where a modern theme declares its design system. WordPress reads it and generates CSS custom properties, so the same values apply on the front end and inside the editor without maintaining two stylesheets.
json
{
"$schema": "https://schemas.wp.org/wp/6.6/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "720px",
"wideSize": "1200px"
},
"color": {
"custom": false,
"palette": [
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#111111", "name": "Contrast" },
{ "slug": "accent", "color": "#0b5cff", "name": "Accent" }
]
},
"typography": {
"fluid": true,
"customFontSize": false
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
}
}
}
Three things to note.
The version key is not the WordPress version. It selects the schema WordPress uses to interpret the file. Version 3 requires WordPress 6.6 or later; version 2 has been the default since 5.9. Set the $schema line to match the minimum WordPress version your theme supports, so your editor validates against the right feature set.
theme.json works in classic themes too. You do not need to rebuild a theme to adopt it. Adding the file gives you a design system the block editor respects, which is the hybrid approach described earlier.
Setting custom: false on colour, and its equivalents for typography and spacing, removes the arbitrary pickers from the editor. That is how you keep a design system intact once a marketing team starts publishing.
What is the template hierarchy and how does it work?
The template hierarchy decides which template file renders a given request. WordPress checks from most specific to most general and uses the first match.
For a single post, the order runs: single-{post-type}-{slug}, then single-{post-type}, then single, then singular, then index. The same logic applies to archives, taxonomies, authors and search results.
The hierarchy is identical in both architectures. Only the file extension changes: single.php in a classic theme, templates/single.html in a block theme.
Two practical consequences. First, index is the fallback that always exists, which is why it is the one required template. Second, you only need to create the templates where behaviour actually differs. Adding category.php when it would be identical to archive.php gives you a second file to maintain and no benefit.
How do you output dynamic content?
In a classic theme, content is rendered through the Loop.
php
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<article <?php post_class(); ?>>
<h2>
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<?php the_excerpt(); ?>
</article>
<?php endwhile; ?>
<?php the_posts_pagination(); ?>
<?php else : ?>
<p><?php esc_html_e( 'Nothing found.', 'acme' ); ?></p>
<?php endif; ?>
Template tags such as the_title() and the_content() output data directly. Their get_ counterparts return it instead, which is what you want when the value needs processing before display.
For content outside the main query, use WP_Query and always reset afterwards:
php
$featured = new WP_Query( array(
'post_type' => 'post',
'posts_per_page' => 3,
'meta_key' => '_acme_featured',
'meta_value' => '1',
) );
if ( $featured->have_posts() ) {
while ( $featured->have_posts() ) {
$featured->the_post();
// output
}
wp_reset_postdata();
}
In block themes, most of this is handled by the Query Loop block, and custom output belongs in a registered block or a block binding rather than in a template file.
Hooks are how you extend behaviour without editing core or fighting your own templates later. Actions insert behaviour at defined points; filters modify values in transit. Both survive updates, which hardcoded changes do not.
What are the security practices that matter?
Two rules cover most of what goes wrong in themes.
Escape on output, always, using the function that matches the context.
php
echo esc_html( $title );
echo '<a href="' . esc_url( $link ) . '">';
printf( '<img src="%s" alt="%s">', esc_url( $src ), esc_attr( $alt ) );
echo wp_kses_post( $rich_text );
Sanitise and verify on input. Validate what comes in, verify a nonce on any form or AJAX request, and check the user’s capability before acting:
php
if ( ! isset( $_POST['acme_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['acme_nonce'] ), 'acme_action' ) ) {
return;
}
if ( ! current_user_can( 'edit_posts' ) ) {
return;
}
$value = sanitize_text_field( wp_unslash( $_POST['acme_field'] ) );
Use $wpdb->prepare() for any direct database query, and prefer the WordPress API over direct SQL wherever one exists. Most theme vulnerabilities in the wild come from unescaped output and missing capability checks, not from anything exotic.
How do you test and debug a theme?
Enable debugging in wp-config.php on your development environment, and never on production:
php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', true );
Beyond that, four checks catch most problems before a client finds them.
Test with realistic content. The theme unit test data published by the WordPress community includes long titles, missing featured images, deeply nested lists, unusual post formats and awkward comment threads. Layouts that look fine with three tidy demo posts break on real editorial content.
Watch your queries. Query Monitor shows query counts, slow queries, hook execution and enqueued assets on every page load. A template that quietly runs eighty queries is easier to fix during development than after launch.
Run Theme Check for coding standards and deprecated function usage, and run PHP_CodeSniffer against the WordPress standard as part of your workflow.
Test accessibility properly. Automated tools catch contrast ratios and missing labels. Keyboard navigation, focus order and screen reader behaviour need manual testing. This is a functional requirement in most markets now, not a refinement.
Common mistakes
- Hardcoding stylesheets and scripts into templates instead of enqueueing them
- Leaving a closing
?>in PHP files - Skipping escaping because the value “comes from our own database”
- Creating template files that duplicate the fallback without changing anything
- Building a theme without
theme.json, then fighting block editor defaults in CSS - Editing a third-party theme directly instead of using a child theme
- Treating accessibility and performance as a phase after the design is signed off
FAQ
What files are required for a WordPress theme?
A classic theme requires style.css and index.php. A block theme requires style.css and templates/index.html. In both cases functions.php and theme.json are strongly recommended for anything beyond a demonstration.
Does functions.php need an opening PHP tag?
Yes. The opening <?php is required. The convention is to omit the closing ?> at the end of the file, because trailing whitespace after it can send output before headers and break the site.
Should I build a block theme or a classic theme?
Build a block theme when editors need real control over layout and templates. Build a classic or hybrid theme when templates carry substantial custom logic, when you need tight control over what loads, or when the brief calls for a deliberately constrained editing experience. A classic theme with theme.json is a reasonable middle position.
Can I add theme.json to an existing classic theme?
Yes, and it is usually worth doing. The file gives you a design system that the block editor respects, without moving templates into the Site Editor or rebuilding anything.
Do I need a child theme?
Only when you are extending a theme you do not control. If you are building your own theme, a child theme adds a layer without adding anything. When customising a third-party theme, a child theme is what stops the next update from erasing your work.
How long does it take to build a custom theme?
For a straightforward brochure site with an established design, days. For a platform with custom post types, complex templates and integrations, weeks to months. The variable is rarely the theme layer itself. It is the content model, the integrations and the number of distinct page types.
Building something where the theme layer is the smallest part of the problem? We start with the content model and the architecture. → Talk to us



