Subscribe to Our Mailing List and Stay Up-to-Date! Subscribe

WordPress Theme Development from Scratch: Complete Tutorial 2025

WordPress theme development transforms design concepts into functional WordPress interfaces through template files, functions.php, style.css, and template hierarchy. From basic index.php and style.css requirements to advanced custom templates, post types integration, and theme customization APIs, custom themes provide complete design control beyond pre-built options. This comprehensive guide teaches theme file structure, template hierarchy, WordPress loops, navigation menus, widget areas, and modern theme development workflows.

Theme File Structure

Minimum Required Files:

mytheme/
├── style.css       (required)
├── index.php       (required)
├── screenshot.png  (recommended)

Complete Theme Structure:

mytheme/
├── style.css
├── index.php
├── functions.php
├── header.php
├── footer.php
├── sidebar.php
├── single.php
├── page.php
├── archive.php
├── search.php
├── 404.php
├── comments.php
├── screenshot.png
├── template-parts/
│   ├── content.php
│   └── content-none.php
├── inc/
│   ├── custom-header.php
│   └── template-tags.php
├── js/
│   └── custom.js
├── css/
│   └── custom.css
└── languages/
    └── mytheme.pot

Style.css Header

Required Theme Metadata:

/*
Theme Name: My Custom Theme
Theme URI: https://example.com/my-theme
Author: Your Name
Author URI: https://example.com
Description: A custom WordPress theme built from scratch
Version: 1.0.0
Requires at least: 6.0
Tested up to: 6.4
Requires PHP: 7.4
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: mytheme
Tags: custom-background, custom-logo, custom-menu, featured-images, threaded-comments
*/

Only Theme Name required, but all recommended.

Index.php Template

Basic index.php Structure:

<?php get_header(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main">

        <?php
        if (have_posts()) :
            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>
                    </header>

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

                    <footer class="entry-footer">
                        <span class="posted-on"><?php echo get_the_date(); ?></span>
                        <span class="byline"> by <?php the_author(); ?></span>
                    </footer>
                </article>
                <?php
            endwhile;

            the_posts_navigation();
        else :
            ?>
            <p><?php esc_html_e('No posts found', 'mytheme'); ?></p>
        <?php endif; ?>

    </main>
</div>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Header.php Template

Standard Header Structure:

<!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(); ?>

<div id="page" class="site">
    <a class="skip-link screen-reader-text" href="#primary"><?php esc_html_e('Skip to content', 'mytheme'); ?></a>

    <header id="masthead" class="site-header">
        <div class="site-branding">
            <?php
            if (has_custom_logo()) :
                the_custom_logo();
            else :
                ?>
                <h1 class="site-title">
                    <a href="<?php echo esc_url(home_url('/')); ?>"><?php bloginfo('name'); ?></a>
                </h1>
                <p class="site-description"><?php bloginfo('description'); ?></p>
            <?php endif; ?>
        </div>

        <nav id="site-navigation" class="main-navigation">
            <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
                <?php esc_html_e('Menu', 'mytheme'); ?>
            </button>
            <?php
            wp_nav_menu(array(
                'theme_location' => 'primary',
                'menu_id'        => 'primary-menu',
            ));
            ?>
        </nav>
    </header>

    <div id="content" class="site-content">

Footer.php Template

Standard Footer Structure:

    </div><!-- #content -->

    <footer id="colophon" class="site-footer">
        <div class="site-info">
            <a href="<?php echo esc_url(__('https://wordpress.org/', 'mytheme')); ?>">
                <?php printf(esc_html__('Proudly powered by %s', 'mytheme'), 'WordPress'); ?>
            </a>
            <span class="sep"> | </span>
            <?php printf(esc_html__('Theme: %1$s by %2$s.', 'mytheme'), 'My Custom Theme', '<a href="https://example.com">Your Name</a>'); ?>
        </div>
    </footer>
</div><!-- #page -->

<?php wp_footer(); ?>

</body>
</html>

Functions.php Setup

Essential Functions:

<?php
/**
 * Theme Functions
 */

if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

/**
 * Theme Setup
 */
function mytheme_setup() {
    // Add default posts and comments RSS feed links to head
    add_theme_support('automatic-feed-links');

    // Let WordPress manage the document title
    add_theme_support('title-tag');

    // Enable support for Post Thumbnails
    add_theme_support('post-thumbnails');

    // Register navigation menus
    register_nav_menus(array(
        'primary' => esc_html__('Primary Menu', 'mytheme'),
        'footer'  => esc_html__('Footer Menu', 'mytheme'),
    ));

    // Switch default core markup to HTML5
    add_theme_support('html5', array(
        'search-form',
        'comment-form',
        'comment-list',
        'gallery',
        'caption',
        'style',
        'script',
    ));

    // Add theme support for selective refresh for widgets
    add_theme_support('customize-selective-refresh-widgets');

    // Add support for custom logo
    add_theme_support('custom-logo', array(
        'height'      => 100,
        'width'       => 400,
        'flex-height' => true,
        'flex-width'  => true,
    ));

    // Add support for custom background
    add_theme_support('custom-background', array(
        'default-color' => 'ffffff',
    ));

    // Add support for Block Editor styles
    add_theme_support('wp-block-styles');

    // Add support for full and wide align images
    add_theme_support('align-wide');

    // Add support for editor styles
    add_theme_support('editor-styles');

    // Enqueue editor styles
    add_editor_style('style-editor.css');

    // Add support for responsive embeds
    add_theme_support('responsive-embeds');
}
add_action('after_setup_theme', 'mytheme_setup');

/**
 * Set content width
 */
function mytheme_content_width() {
    $GLOBALS['content_width'] = apply_filters('mytheme_content_width', 800);
}
add_action('after_setup_theme', 'mytheme_content_width', 0);

/**
 * Enqueue scripts and styles
 */
function mytheme_scripts() {
    // Main stylesheet
    wp_enqueue_style('mytheme-style', get_stylesheet_uri(), array(), '1.0.0');

    // Custom stylesheet
    wp_enqueue_style('mytheme-custom', get_template_directory_uri() . '/css/custom.css', array(), '1.0.0');

    // Custom JavaScript
    wp_enqueue_script('mytheme-navigation', get_template_directory_uri() . '/js/navigation.js', array(), '1.0.0', true);

    // Comment reply script
    if (is_singular() && comments_open() && get_option('thread_comments')) {
        wp_enqueue_script('comment-reply');
    }
}
add_action('wp_enqueue_scripts', 'mytheme_scripts');

/**
 * Register widget areas
 */
function mytheme_widgets_init() {
    register_sidebar(array(
        'name'          => esc_html__('Sidebar', 'mytheme'),
        'id'            => 'sidebar-1',
        'description'   => esc_html__('Add widgets here.', 'mytheme'),
        'before_widget' => '<section id="%1$s" class="widget %2$s">',
        'after_widget'  => '</section>',
        'before_title'  => '<h2 class="widget-title">',
        'after_title'   => '</h2>',
    ));

    register_sidebar(array(
        'name'          => esc_html__('Footer', 'mytheme'),
        'id'            => 'footer-1',
        'description'   => esc_html__('Add footer widgets here.', 'mytheme'),
        'before_widget' => '<div id="%1$s" class="widget %2$s">',
        'after_widget'  => '</div>',
        'before_title'  => '<h3 class="widget-title">',
        'after_title'   => '</h3>',
    ));
}
add_action('widgets_init', 'mytheme_widgets_init');

Single.php Template

Single Post Template:

<?php get_header(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main">

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

            <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                <header class="entry-header">
                    <?php the_title('<h1 class="entry-title">', '</h1>'); ?>

                    <div class="entry-meta">
                        <span class="posted-on">
                            <?php echo get_the_date(); ?>
                        </span>
                        <span class="byline">
                            by <?php the_author_posts_link(); ?>
                        </span>
                        <span class="cat-links">
                            <?php the_category(', '); ?>
                        </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_content();

                    wp_link_pages(array(
                        'before' => '<div class="page-links">' . esc_html__('Pages:', 'mytheme'),
                        'after'  => '</div>',
                    ));
                    ?>
                </div>

                <footer class="entry-footer">
                    <?php the_tags('<span class="tags-links">', ', ', '</span>'); ?>
                </footer>
            </article>

            <?php
            // Previous/next post navigation
            the_post_navigation(array(
                'prev_text' => '<span class="nav-subtitle">' . esc_html__('Previous:', 'mytheme') . '</span> <span class="nav-title">%title</span>',
                'next_text' => '<span class="nav-subtitle">' . esc_html__('Next:', 'mytheme') . '</span> <span class="nav-title">%title</span>',
            ));

            // Comments
            if (comments_open() || get_comments_number()) :
                comments_template();
            endif;

        endwhile;
        ?>

    </main>
</div>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Page.php Template

Static Page Template:

<?php get_header(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main">

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

            <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                <header class="entry-header">
                    <?php the_title('<h1 class="entry-title">', '</h1>'); ?>
                </header>

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

                    wp_link_pages(array(
                        'before' => '<div class="page-links">' . esc_html__('Pages:', 'mytheme'),
                        'after'  => '</div>',
                    ));
                    ?>
                </div>

                <?php if (comments_open() || get_comments_number()) : ?>
                    <?php comments_template(); ?>
                <?php endif; ?>
            </article>

        <?php endwhile; ?>

    </main>
</div>

<?php get_footer(); ?>

Archive.php Template

Archive Listing Template:

<?php get_header(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main">

        <?php if (have_posts()) : ?>

            <header class="page-header">
                <?php
                the_archive_title('<h1 class="page-title">', '</h1>');
                the_archive_description('<div class="archive-description">', '</div>');
                ?>
            </header>

            <?php
            while (have_posts()) :
                the_post();
                get_template_part('template-parts/content', 'excerpt');
            endwhile;

            the_posts_navigation();

        else :
            get_template_part('template-parts/content', 'none');
        endif;
        ?>

    </main>
</div>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Template Parts

template-parts/content-excerpt.php:

<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
    <header class="entry-header">
        <?php the_title(sprintf('<h2 class="entry-title"><a href="%s">', esc_url(get_permalink())), '</a></h2>'); ?>

        <div class="entry-meta">
            <span class="posted-on"><?php echo get_the_date(); ?></span>
            <span class="byline"> by <?php the_author(); ?></span>
        </div>
    </header>

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

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

    <footer class="entry-footer">
        <a href="<?php the_permalink(); ?>" class="read-more">
            <?php esc_html_e('Read More', 'mytheme'); ?>
        </a>
    </footer>
</article>

Sidebar.php Template

Widget Area Sidebar:

<?php
if (!is_active_sidebar('sidebar-1')) {
    return;
}
?>

<aside id="secondary" class="widget-area">
    <?php dynamic_sidebar('sidebar-1'); ?>
</aside>

Search.php Template

Search Results Template:

<?php get_header(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main">

        <?php if (have_posts()) : ?>

            <header class="page-header">
                <h1 class="page-title">
                    <?php printf(esc_html__('Search Results for: %s', 'mytheme'), '<span>' . get_search_query() . '</span>'); ?>
                </h1>
            </header>

            <?php
            while (have_posts()) :
                the_post();
                get_template_part('template-parts/content', 'search');
            endwhile;

            the_posts_navigation();

        else :
            ?>
            <section class="no-results not-found">
                <header class="page-header">
                    <h1 class="page-title"><?php esc_html_e('Nothing Found', 'mytheme'); ?></h1>
                </header>

                <div class="page-content">
                    <p><?php esc_html_e('Sorry, but nothing matched your search terms. Please try again with different keywords.', 'mytheme'); ?></p>
                    <?php get_search_form(); ?>
                </div>
            </section>
        <?php endif; ?>

    </main>
</div>

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Conclusion

WordPress theme development creates custom designs through template hierarchy, functions.php configuration, and modular template parts. Build themes from scratch using required style.css and index.php files, expand functionality through header.php, footer.php, single.php, page.php templates, register navigation menus and widget areas, enqueue stylesheets and scripts properly, and follow WordPress coding standards. Custom themes provide unlimited design flexibility and complete control over WordPress site appearance and functionality.

  1. WordPress Theme Handbook
  2. Template Hierarchy
  3. Theme Development Checklist
  4. WordPress Coding Standards
  5. Underscores Starter Theme

Call to Action

Custom themes need reliable backups. Backup Copilot Pro protects your WordPress theme files and database automatically. Safeguard your development work—start your free 30-day trial today!