# How to Create Your Own WordPress Theme from Scratch (2026)

- Link: https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/
- Published: 2026-10-23T10:20:12+03:00
- Author: eJohnny

Building your own WordPress theme is one of the most useful skills a developer can
have. It gives you full control over how a site looks and behaves, removes the bloat
of off-the-shelf themes, and makes you faster at every future project. The good 
news: the barrier to entry is lower than you think. A working theme needs just two
files.

This guide covers everything from the bare minimum to a production-ready theme: 
what files you need, how the template hierarchy works, what to keep in mind, and
concrete performance and security tips. Code examples are included throughout.

**Note:** WordPress now supports two theme types — _classic_ (PHP templates) and
_block_ (HTML templates for Full Site Editing). This guide focuses on classic themes,
which are still the best starting point for learning. Block themes are covered in
the “What to keep in mind” section.

![Laptop with code on screen for WordPress theme development](https://images.unsplash.
com/photo-1498050108023-c5249f4df085?fm=jpg&q=60&w=1200&auto=format&fit=crop)

## What is a WordPress theme?

A theme is a collection of files that determines how your WordPress site looks and
how content is displayed. WordPress ships with a default theme (currently Twenty
Twenty-Five). When you activate your own theme, WordPress swaps the templates but
keeps your content, settings, and plugins intact.

A theme is not a plugin. Themes handle _presentation_ (layout, design, typography).
Plugins handle _functionality_ (forms, SEO, caching). Keep that separation in mind—
it will save you from building themes that break when deactivated.

## The bare minimum: two files

WordPress recognizes a theme if it contains at least these two files in a folder
under `wp-content/themes/your-theme/`:

 1. `style.css` — must contain a theme header comment. This is how WordPress identifies
    the theme (name, author, version, etc.).
 2. `index.php` — the fallback template. WordPress uses it when no more specific template
    exists.

### style.css (the header)

    ```
    /* Theme Name: My Theme Theme URI: https://example.com/my-theme Author: Your Name Author URI: https://example.com Description: A minimal WordPress theme built from scratch. Version: 1.0.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Text Domain: my-theme */  /* Your CSS goes below */ body {   font-family: system-ui, -apple-system, sans-serif;   line-height: 1.6;   color: #1a1a1a;   max-width: 720px;   margin: 0 auto;   padding: 2rem 1rem; }
    ```

The header comment is mandatory. Without `Theme Name`, WordPress will not list the
theme in the admin. `Text Domain` is used for translations (more on that later).

### index.php (the fallback)

    ```
    <?php /**  * The main template file.  *  * @package My_Theme  */  get_header();  if ( have_posts() ) :     while ( have_posts() ) :         the_post();         ?>         <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>             <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>             <div class="entry-content"><?php the_excerpt(); ?></div>         </article>         <?php     endwhile;     the_posts_navigation(); else :     echo '<p>No posts found.</p>'; endif;  get_footer();
    ```

That is a complete, working theme. Upload the folder to `wp-content/themes/`, activate
it in Appearance → Themes, and you have a live site. Ugly, but functional.

## Building it out: the standard file set

A real theme splits responsibilities across files. Here is the file structure you
will grow into:

    ```
    my-theme/ ├── style.css          (required: theme header + styles) ├── index.php          (required: fallback template) ├── functions.php      (theme setup, enqueues, features) ├── header.php         (site header, <head>, opening markup) ├── footer.php         (closing markup, footer) ├── single.php         (single post) ├── page.php           (single page) ├── archive.php        (category, tag, date, author archives) ├── search.php         (search results) ├── 404.php            (not found) ├── comments.php       (comment form + list) ├── sidebar.php        (widget area) └── screenshot.png     (preview image in admin, 1200x900 recommended)
    ```

### header.php

    ```
    <?php /**  * The header for our theme.  */ ?> <!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(); ?>> <header class="site-header">     <h1><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php bloginfo( 'name' ); ?></a></h1>     <p><?php bloginfo( 'description' ); ?></p>     <nav><?php wp_nav_menu( array( 'theme_location' => 'primary' ) ); ?></nav> </header> <main class="site-main">
    ```

### footer.php

    ```
        </main>     <footer class="site-footer">         <p>&copy; <?php echo date( 'Y' ); ?> <?php bloginfo( 'name' ); ?></p>         <?php wp_footer(); ?>     </footer> </body> </html>
    ```

### functions.php

    ```
    <?php /**  * Theme functions and definitions.  */ function my_theme_setup() {     // Let WordPress handle the document title.     add_theme_support( 'title-tag' );      // Enable featured images.     add_theme_support( 'post-thumbnails' );      // Register a navigation menu.     register_nav_menus( array(         'primary' => esc_html__( 'Primary Menu', 'my-theme' ),     ) );      // Add support for HTML5 markup.     add_theme_support( 'html5', array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' ) );      // Add support for wide/full alignment in the block editor.     add_theme_support( 'align-wide' ); } add_action( 'after_setup_theme', 'my_theme_setup' );  function my_theme_scripts() {     // Main stylesheet.     wp_enqueue_style( 'my-theme-style', get_stylesheet_uri(), array(), wp_get_theme()->get( 'Version' ) );      // Main JavaScript (in footer).     wp_enqueue_script( 'my-theme-script', get_template_directory_uri() . '/js/main.js', array(), wp_get_theme()->get( 'Version' ), true ); } add_action( 'wp_enqueue_scripts', 'my_theme_scripts' );  // Register a widget area. function my_theme_widgets() {     register_sidebar( array(         'name'          => esc_html__( 'Sidebar', 'my-theme' ),         'id'            => 'sidebar-1',         'description'   => esc_html__( 'Add widgets here.', 'my-theme' ),         'before_widget' => '<section id="%1$s" class="widget %2$s">',         'after_widget'  => '</section>',         'before_title'  => '<h2 class="widget-title">',         'after_title'   => '</h2>',     ) ); } add_action( 'widgets_init', 'my_theme_widgets' );
    ```

Two rules here: never hardcode `<link>` or `<script>` tags in `header.php` — always
use `wp_enqueue_style()` / `wp_enqueue_script()`. And always call `wp_head()` and`
wp_footer()` — they are the hooks where WordPress injects core scripts, styles, 
and plugin output.

## The template hierarchy (what to keep in mind)

WordPress decides which template file to load based on the URL. It looks for the
most specific file first, then falls back to more generic ones. The final fallback
is always `index.php`.

 * **Single post:** `single-{post-type}-{slug}.php` → `single-{post-type}.php` →`
   single.php` → `singular.php` → `index.php`
 * **Single page:** custom template → `page-{slug}.php` → `page-{id}.php` → `page.
   php` → `singular.php` → `index.php`
 * **Archive:** `category-{slug}.php` → `category-{id}.php` → `category.php` → `
   archive.php` → `index.php`
 * **Home (blog):** `home.php` → `index.php`
 * **Front page:** `front-page.php` → `home.php` → `page.php` → `index.php`
 * **Search:** `search.php` → `index.php`
 * **404:** `404.php` → `index.php`

You do not need all of these. Start with `index.php`, `single.php`, `page.php`, 
and `archive.php`. Add more only when you need custom layouts for specific content
types.

## Classic vs block themes

Since WordPress 5.9, you can build **block themes** — themes whose templates are
HTML files made of block markup, edited visually in the Site Editor (Full Site Editing).
They use `theme.json` for global styles instead of CSS.

 * **Classic themes** (PHP templates) — what this guide teaches. Best for learning,
   full PHP control, works with all plugins.
 * **Block themes** (HTML templates + `theme.json`) — the future of WordPress. No
   PHP in templates, visual editing, but a steeper learning curve for dynamic logic.
 * **Hybrid** — a classic theme that adds `theme.json` for global styles and block
   support. A good middle ground.

If you are starting fresh in 2026, learn classic first, then migrate to block themes.
The concepts (template hierarchy, the Loop, hooks) transfer directly.

## What to keep in mind

 * **Use child themes for modifications.** Never edit a parent theme directly — 
   updates will overwrite your changes. Create a child theme with just `style.css`(
   with `Template: parent-theme-slug`) and `functions.php`.
 * **Prefix everything.** Function names, CSS classes, option names, custom post
   types — prefix with your theme slug (e.g., `mytheme_`) to avoid collisions with
   plugins and other themes.
 * **Use translation functions.** Wrap all user-facing strings in `esc_html__()`,`
   esc_attr__()`, or `_e()` with your text domain. This makes your theme translatable.
 * **Follow the coding standards.** WordPress has official PHP, CSS, JS, and HTML
   coding standards. Use PHPCS with the WordPress ruleset to lint your code.
 * **Test with real content.** Import the [Theme Unit Test data](https://github.com/WPTRT/theme-unit-test)
   to check edge cases: long titles, no featured image, nested comments, sticky 
   posts, pagination.
 * **Support the block editor.** Even classic themes should add `add_theme_support('
   editor-styles' )` and enqueue an editor stylesheet so the backend matches the
   frontend.
 * **Do not put functionality in themes.** Custom post types, shortcodes, and metaboxes
   belong in plugins. Themes should be swappable without losing content.

## Performance tips

 * **Enqueue, never hardcode.** Use `wp_enqueue_style()` and `wp_enqueue_script()`
   so WordPress can manage dependencies, minify, and conditionally load.
 * **Load scripts in the footer.** Pass `true` as the last argument to `wp_enqueue_script()`
   for non-critical JS. This improves First Contentful Paint.
 * **Use **`wp_get_theme()->get('Version')`** as the version string.** This busts
   the cache automatically when you update the theme.
 * **Conditionally enqueue.** Only load scripts/styles on pages that need them. 
   Check `is_single()`, `is_page()`, etc. before enqueuing.
 * **Minify and combine.** Use a build tool (Vite, Webpack, or `@wordpress/scripts`)
   to minify CSS/JS and generate sourcemaps. WordPress 6.9 also inlines small block
   stylesheets automatically.
 * **Optimize images.** Use `add_image_size()` for custom sizes and serve WebP. 
   Lazy-load images with `loading="lazy"` (WordPress adds this by default since 
   5.5).
 * **Avoid render-blocking resources.** Defer non-critical scripts. Inline critical
   CSS. Use `font-display: swap` for web fonts.
 * **Limit HTTP requests.** Combine small CSS/JS files. Use SVG sprites or icon 
   fonts instead of multiple image files.
 * **Cache aggressively.** Rely on the host’s page cache (Nginx, Varnish) and object
   cache (Redis). Do not build your own caching in the theme.
 * **Profile with Query Monitor.** Install the Query Monitor plugin to find slow
   queries, hook callbacks, and enqueued assets on each page.

## Security tips

 * **Escape output.** Use `esc_html()`, `esc_attr()`, `esc_url()`, `esc_js()` on
   every value you print. Never `echo $variable;` directly.
 * **Sanitize input.** Use `sanitize_text_field()`, `sanitize_email()`, `sanitize_file_name()`,`
   absint()` on all `$_GET`, `$_POST`, `$_REQUEST` values.
 * **Validate data.** Check types, ranges, and allowed values before using input.
   Use `wp_verify_nonce()` for form submissions.
 * **Use nonces for forms.** `wp_nonce_field()` in the form, `wp_verify_nonce()`
   on submission. Prevents CSRF.
 * **Check capabilities.** Use `current_user_can( 'manage_options' )` (or the appropriate
   capability) before allowing admin actions.
 * **Use **`$wpdb->prepare()`**.** Never interpolate variables into SQL queries.
   Always use prepared statements.
 * **Do not expose file paths.** Use `get_template_directory_uri()` (URL) not `get_template_directory()`(
   path) in front-end output.
 * **Disable file editing.** Add `define( 'DISALLOW_FILE_EDIT', true );` in `wp-
   config.php` so themes/plugins cannot be edited from the dashboard.
 * **Keep dependencies updated.** If you use third-party libraries, keep them current.
   Remove unused libraries.
 * **Do not include jQuery from a CDN.** Use the version bundled with WordPress 
   to avoid version conflicts and security issues.
 * **Set proper file permissions.** Folders 755, files 644, `wp-config.php` 600/
   640.

## Tools and workflow

 * **Local environment:** LocalWP, `@wordpress/env`, Docker, or XAMPP. Never develop
   on a live site.
 * **Code editor:** VS Code with the WordPress/PHP extensions, or PhpStorm.
 * **Version control:** Git. Commit often, use branches for features.
 * **WP-CLI:** scaffold themes with `wp scaffold _s my-theme` (Underscores starter).
   Inspect with `wp theme list`.
 * **Testing:** import Theme Unit Test data, test on mobile, test with screen readers,
   run Lighthouse.
 * **Linting:** PHPCS with WordPress ruleset, ESLint for JS, Stylelint for CSS.

## Frequently asked questions

**How long does it take to build a WordPress theme?**
 A minimal theme: a few hours.
A production-ready theme with custom templates, responsive design, and accessibility:
1–4 weeks depending on complexity.

**Do I need to know PHP?**
 Yes, for classic themes. You need basic PHP (loops, 
conditionals, functions) plus WordPress template tags. Block themes require HTML
and `theme.json` but less PHP.

**Should I use a starter theme?**
 For your first theme, yes. Underscores (`_s`)
is the official starter. It gives you a clean, well-structured foundation without
bloat. Once you understand the structure, you can start from scratch.

**Can I sell my theme?**
 Yes. WordPress themes are GPL-licensed, so you can sell
them. You can charge for support, updates, and premium features. ThemeForest and
your own site are common marketplaces.

**Classic or block theme — which should I learn first?**
 Classic. The concepts (
template hierarchy, the Loop, hooks) are foundational and transfer to block themes.
Block themes are the future, but classic is easier to debug and reason about.

## Conclusion

Building a WordPress theme from scratch comes down to two files to start (`style.
css` with a header, and `index.php`), then growing into the template hierarchy, `
functions.php`, and the standard template files. Keep presentation in the theme 
and functionality in plugins, escape everything you output, enqueue assets properly,
and test with real content.

Start small: get `index.php` and `style.css` working, then add `header.php`, `footer.
php`, and `functions.php`. Add templates one at a time as you need them. The WordPress
Theme Handbook and the Theme Unit Test data are your best companions. Happy theming.

### Share this:

 * [  X ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=twitter)
 * [  Facebook ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=facebook)
 * [More](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/#)

 * [  LinkedIn ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=linkedin)
 * [  Reddit ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=reddit)
 * [  Tumblr ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=tumblr)
 * [  Pinterest ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=pinterest)
 * [  Telegram ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=telegram)
 * [  WhatsApp ](https://dev.ejohnny.ro/how-to-create-wordpress-theme-from-scratch/?share=jetpack-whatsapp)
