WordPress Theme Development from Scratch: A Complete Guide for 2023
Table of Contents
Quick Links
- Hosting
- WordPress Docs
- Recommended Plugins
Get posts directly to your inbox
Why Kegan?
Kegan is an army of one. We've worked with him on numerous WordPress projects, 2018-2021 and counting. He's always current on tech and techniques, extremely responsive and thorough, and his infectious optimism fuels our projects forward. He's a rare find. Travis Culwell - AAA
17 Reviews, 5 Stars
Table of Contents
Introduction
This is a guide to help you make your first WordPress theme from scratch. Before we begin, I’m going to make a few assumptions.
First, I’m going to assume you have basic level knowledge of HTML, CSS, some JavaScript, and PHP. While you don’t need to be super proficient in any, I’m going to assume you can at least read and write them on an elementary level.
Second, I’m going to assume you’ve worked with WordPress in some capacity. Not in a theme development capacity, but at least enough to know how the backend flow works from a content point-of-view.
So, with that being said lets learn WordPress theme development from scratch!
Local Development Setup
A local development setup means that you’re installing WordPress on your computer. You can view and edit your site within a browser, but you don’t have to be connected to the internet.
This way you can make changes and updates to the code base before you push them to a live site.
There are a lot of different wants to setup your WordPress site locally, and I’m not going to go too far into detail on all of them.
The App I use (and recommend) is called Local by Flywheel. They make it super simple to get everything up and running without needing to know a lot of tech.
If you prefer something else: use WAMP if you’re running windows, if you’re running linux you can use LAMP, and if you’re running a mac you can use MAMP.
All of these will install PHP locally, and run a web server. They’re slightly more complicated to setup and manage, but allow for greater customization. Once they’re installed and running you can then install WordPress.
Since this article won’t cover too many server configurations or considerations, I’m not going to go into all the customizations that are possible.
Now that we’ve got our WordPress setup locally, lets take a look at actual WordPress theme development from scratch.
The Classic Editor
Up until a few years ago WordPress had a pretty basic WYSIWYG editor. It was a little rich text editor, but didn’t allow for much in terms of page layouts or customizations.
As a developer who hands sites off to clients, the classic editor is, in many ways, easier to manage. It limits the inputs a user can have, and is very easy to maintain and build upon as a developer.
I would be remiss to not at least mention theme building with the old editor, because that’s the foundation of the new system. Even though we’re moving towards a more plugin and block based building system, themes won’t go away for a long, long time. Much of that is in part to the old editor.
The main carry over is the file structure, but knowing the PHP-way of building and maintaining themes is a good idea as they won’t go away for a long, long time.
File structure
Building themes on top of the classic editor requires PHP files. Let’s take a look at a basic file structure:
- index.php: this is generally used as the fall back template, incase something isn’t defined
- style.css: this is where all your theme info lives (and sometimes styles too)
- functions.php: this is where all your custom functions live
These are the three main files, and in fact you don’t need a functions.php file for a theme to be valid.
But going even further, we add some more files to customize the front-end rendering based on what the user is viewing (again, this is a rather bare-bones list of files we’ll want in a theme):
- header.php: the header file to be rendered within each template by calling
get_header()
- footer.php: the footer file to be rendered within each template by calling
get_footer()
- page.php: the template for rendering pages
- sidebar.php: the template to be rendered when using
get_sidebar()
- single.php: the template used for all single posts
- 404.php: the template for 404 errors
- archive.php: the template for all archives
- search.php: the template for search results
The template that is rendered on the front end will depend upon how detailed your theme files are.
WordPress has a good diagram in its documentation with regards to template hierarchy.
But, what exactly are in these files? Lets take a look.
style.css
/*!
Theme Name: Kegan Quimby Website
Theme URI: https://keganquimby.com
Author: Kegan Quimby
Author URI: https://keganquimby.com
Description: Basic WP framework
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: LICENSE
Text Domain: REAL
Tags: custom-background, custom-logo, custom-menu, featured-images, threaded-comments, translation-ready
*/
This is the most basic version of style.css. It lets WordPress know the theme info associated with your theme.
We won’t enqueue this file, but rather just allow it to contain our theme info.
Instead, we’ll use Webpack to compile our CSS into a single, minified, file in our /dist/
folder that we will enqueue (see functions.php).
index.php
<?php
/**
** get_header() pulls in the header.php file. it also allows header hooks to run
** making it possible for scripts, styles, etc. to load
*/
get_header();
?>
<?php
/**
** if (have_posts()) checks to see if any posts exist,
** before we loop through them
*/
if (have_posts()) :
?>
<h1>Index Template</h1>
<ul>
<?php
/**
** while(have_posts()) : the_post() sets up all the post data
** so we can loop through posts and
** display information associated with each
*/
while(have_posts()) : the_post();
?>
<li><a href="<?php the_permalink(); ?><?php the_title(); ?></a></li>
<?php
/**
** the_permalink() and the_title() are two examples
** of functions available
** within the loop (they link to each post by showing their title)
*/
?>
<?php endwhile; ?>
</ul>
<?php endif; ?>
<?php
/**
** get_footer() pulls in footer.php and
** lets hooks and filters run in the footer
*/
get_footer(); ?>
I’ve commented each line so you can understand exactly what is going on.
This is a very basic, simple, easy-to-digest index template. Most likely your production ready templates won’t be this simple.
But at its core is the WordPress loop. The WordPress loop is the foundation of all templates. For single items the loop will output the given information for a single post, page, etc. whereas in an archive template the loop will loop through the given posts or pages matching the specific criteria based on the template heirarchy.
Index.php is typically the fall back template (meaning if nothing else is in effect, WordPress will pull the index template). Lets take a look at some more specific templates and template parts.
functions.php
I like to move all my functions into separate files so the functions file isn’t 2,000 lines of hard-to-find code. It would look something like this:
<?php
require get_template_directory() . '/inc/scripts.php';
require get_template_directory() . '/inc/widgets.php';
require get_template_directory() . '/inc/menus.php';
require get_template_directory() . '/inc/images.php';
Note: this is a super bare-bones list for demo purposes. Typically I would also include things related to building blocks (see more below), any AJAX calls, custom post types and taxonomies, and any number of other things.
But for now, let’s break down these files.
/inc/scripts.php
This file will include all of your scripts and styles that will be output on the front end of your theme. I also dequeue all Gutenberg related styles, so that I can custom code my own.
There are also other reasons for dequeuing or enqueuing scripts for WordPress performance considerations.
function kq_theme_scripts_styles() {
wp_enqueue_style('main-style', get_template_directory_uri() . '/dist/main.min.css', '', '1');
}
add_action( 'wp_enqueue_scripts', 'kq_theme_scripts_styles' );
//Remove Gutenberg Block Library CSS from loading on the frontend
function smartwp_remove_wp_block_library_css(){
wp_dequeue_style( 'wp-block-library' );
wp_dequeue_style( 'wp-block-library-theme' );
wp_dequeue_style( 'wc-block-style' );
}
add_action( 'wp_enqueue_scripts', 'smartwp_remove_wp_block_library_css', 100 );
/inc/widgets.php
This file includes all the functions related to setting up widgets in the widgets section of the WordPress admin panel (we’ll later output them in footer.php).
<?php
function register_widgets() {
register_sidebar( array(
'name' => 'Footer Widgets',
'id' => 'footer_widgets',
'before_widget' => '<div>',
'after_widget' => '</div>',
'before_title' => '<h5>',
'after_title' => '</h5>',
) );
register_sidebar( array(
'name' => 'Blog Sidebar',
'id' => 'sidebar-1',
'before_widget' => '<div>',
'after_widget' => '</div>',
'before_title' => '<h4>',
'after_title' => '</h4>',
) );
}
add_action( 'widgets_init', 'register_widgets' );
/inc/menus.php
This file will include all the menus you want to be editable in the Appearance > Menus section of WordPress. For this demo, I have just two: the main navigation menu & a footer navigation menu.
We’re also using a custom menu structure (as seen with the Bootstrap_Walker
class). This allows us to output custom HTML, instead of the default WordPress HTML.
You’ll notice in the header.php file below that we pass in header-menu
as the value for the theme_location
key in the wp_nav_menu
function.
<?php
function register_menus() {
register_nav_menus(
array(
'header-menu' => __( 'Main Menu' ),
'footer-menu' => __( 'Footer Menu' )
)
);
}
add_action( 'init', 'register_menus' );
class Bootstrap_Walker extends Walker_Nav_Menu {
function start_el(&$output, $item, $depth=0, $args=array(), $id = 0) {
$object = $item->object;
$type = $item->type;
$title = $item->title;
$description = $item->description;
$permalink = $item->url;
$output .= "<li class='nav-item " . implode(" ", $item->classes) . "'>";
}
}
/inc/images.php
This is a super simple file used to let WordPress know we want post thumbnails associated with our theme.
You can also use this file if you want to add different image sizes for cropping purposes. Images often cause a lot of bloat, so you’ll want to make sure you’re serving the right sized images throughout you site. It’s generally the best thing you can do to speed up your website without using any plugins.
<?php
add_theme_support('post-thumbnails');
All of these function files use hooks and filters to make WordPress a lot more extensible. Hooks and filters are the foundation of WordPress, and you should really familiarize yourself on how to use them.
header.php
<!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="http://gmpg.org/xfn/11">
<link rel='icon' href='favicon.ico' type='image/x-icon'/ >
<link rel="preload" href="<?php echo get_stylesheet_directory_uri(); ?>/dist/main.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="<?php echo get_stylesheet_directory_uri(); ?>/dist/main.min.css">
</noscript>
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<header id="masthead" class="site-header">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-md-6 col-lg logo">
<h2><a href="<?php echo get_home_url(); ?>"><?php echo get_bloginfo('name'); ?></a></h2>
</div><!-- END col md 6 -->
<div class="col-lg-8 col-md-6">
<nav class="navbar navbar-expand-lg">
<input type="checkbox" id="navbar-toggle-cbox">
<label for="navbar-toggle-cbox" class="navbar-toggler collapsed" data-bs-toggle="collapse" data-bs-target="#primary-menu-container">
<span></span><span></span><span></span>
</label>
<?php
wp_nav_menu( array(
'theme_location' => 'header-menu',
'menu_id' => 'primary-menu',
'container_class' => 'collapse navbar-collapse',
'container_id' => 'primary-menu-container',
'menu_class' => 'navbar-nav'
));
?>
</nav><!-- #site-navigation -->
</div><!-- END col md 6 -->
</div><!-- END row -->
</div><!-- END container -->
</header>
The header file contains some PHP in between HTML, and for the most part is self explainatory. But lets take a look at some important functions:
wp_head()
– this function allows scripts, styles, and other header hooks to be utilized.body_class()
– this function outputs various classes on the body element based on the template being viewed.get_home_url()
– this function returns the home URL based on the settings.wp_nav_menu()
– this function outputs the nav menu based on the user setting in appearance > menus (more on how to set these up below).
On top of that, you may notice that we’re directly calling our stylesheet. This is often frowned upon, but in order to add the necessary attributes for preloading (something Google requires for improved page speed), it’s much easier to update and manage this way.
Typically stylesheets and scripts are enqueued via the WordPress function wp_enqueue_script
or wp_enqueue_style
.
Also note the class names, which correspond to Twitter’s Bootstrap CSS framework. This is my framework of choice, but any CSS framework can help you build faster.
footer.php
<footer>
<div class="container">
<div class="row">
<?php if ( is_active_sidebar( 'footer_widgets' ) ) : ?>
<?php dynamic_sidebar( 'footer_widgets' ); ?>
<?php endif; ?>
</div><!-- END row -->
</div><!-- END container -->
<div class="copyright">
<div class="container">
<div class="row text-center">
<div class="col-md-12">
<p>© Copyright <?php echo date("Y");?> <?php echo get_bloginfo('name'); ?>. All Rights Reserved.</p>
</div>
</div><!-- END row -->
</div><!-- END container -->
</div><!-- END copyright -->
</footer>
<?php wp_footer(); ?>
</body>
</html>
This is a footer file at its most basic. Lets take a look at some of the functions:
is_active_sidebar()
– this allows widgets to be pulled in from the widgets section in the admin panel of WordPress. It’s setup from our functions.phpget_bloginfo()
– this function pulls in settings from the WordPress admin (in the footer example above, it’s the site name).wp_footer()
– wp_footer allows for scripts and styles to be queued in the footer (amongst a few other things).
page.php
Pretty self explanatory, but page.php is the front end template for all pages.
<?php get_header(); ?>
<?php if (have_posts()) : ?>
<h1><?php the_title(); ?></h1>
<main class="page-container" id="page-<?php the_ID(); ?>">
<?php while(have_posts()) : the_post(); ?>
<?php the_content(); ?>
<?php endwhile; ?>
</main><!-- END page -->
<?php endif; ?>
<?php get_footer(); ?>
We’ve seen almost all of these function so far, with the exception of the_ID()
. This function will pull the post ID from the given post or page.
single.php
The single.php file is used for single blog posts and other custom post types.
<?php get_header(); ?>
<?php if (have_posts()) : ?>
<h1><?php the_title(); ?></h1>
<main class="page-container" id="page-<?php the_ID(); ?>">
<div class="main-content">
<?php while(have_posts()) : the_post(); ?>
<?php the_content(); ?>
<?php endwhile; ?>
</div><!-- END main content -->
<div class="sidebar">
<?php get_sidebar(); ?>
</div><!-- END sidebar -->
</main><!-- END page -->
<?php endif; ?>
<?php get_footer(); ?>
Because this is so bare bones, it’s very similar to page.php, with the exception that we’re now pulling in a sidebar with get_sidebar()
. This will pull in the sidebar.php file, as seen below.
sidebar.php
The sidebar file is pulled in whenever there is a get_sidebar()
call (see archive.php, single.php, etc).
<?php
if ( ! is_active_sidebar( 'sidebar-1' ) ) {
return;
}
?>
<aside id="secondary" class="widget-area">
<?php dynamic_sidebar( 'sidebar-1' ); ?>
</aside><!-- #secondary -->
First, we check to see that anything is in the sidebar-1
widget section in the backend of WordPress. If there’s nothing, then we just return.
If there’s content in there, then we’ll display it as per the settings in /inc/widgets.php
.
Note: you don’t need to setup the sidebar like this, but it just makes it more maintainable within WordPress itself. You can hard code anything into this file.
404.php
This file is pulled whenever a URL is visited that doesn’t have anything associated with it.
<?php get_header(); ?>
<div class="container">
<div class="row">
<div class="col-md-12 text-center">
<h1>You must be lost.</h1>
<p><a href="<?php home_url(); ?>">Go back home</a>.</p>
</div>
</div>
</div>
<?php get_footer(); ?>
archive.php
This is the file that is used whenever there is a list of posts to be displayed. If there is a more specific file (IE category.php), that file will take precedence over archive.php.
<?php get_header(); ?>
<?php if (have_posts()) : ?>
<main>
<header>
<h1><?php single_cat_title(); ?></h1>
</header>
<div class="container">
<div class="row">
<div class="col-md-4">
<?php get_sidebar(); ?>
</div>
<div class="col-md-8">
<?php while(have_posts()) : the_post(); ?>
<div class="row">
<div class="col">
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<p class="excerpt"><?php echo get_the_excerpt(); ?><br/><a href="<?php the_permalink(); ?>">Read the full post</a></p>
<p class="meta">Posted: <?php echo get_the_date('M d, Y'); ?> · Updated: <?php echo get_the_modified_time('M d, Y'); ?></p>
</div><!-- END col -->
</div><!-- END row -->
<?php endwhile; ?>
</div><!-- END col-md-8 -->
</div><!-- END row -->
</div><!-- END container -->
</main>
<?php endif; ?>
<?php get_footer(); ?>
search.php
This is the template for search results.
<?php get_header(); ?>
<?php if (have_posts()) : ?>
<div class="container">
<?php while (have_posts()) : the_post(); ?>
<p><a href="<?php the_permalink(); ?>"><?php the_title(); ?></p>
<?php endwhile; ?>
</div><!-- END container -->
<?php elseif : ?>
<h2>No posts found</h2>
<?php endif; ?>
<?php get_footer(); ?>
These are all the files that should get you started with WordPress theme development from scratch.