How to build a custom WordPress theme from scratch in 87 easy steps without going crazy

For this site WPCode.org (did you know this is where you are???) we wanted a fully custom theme. And that’s when we thought why not document every step of the process and create a tutorial on how to build a custom WordPress theme from scratch. It turned out to require 87 steps.

Now if you’re new to WP or theming, let’s clarify that there are “base themes” and “child themes”, native WP themes and 3rd party (premium) themes. It’s our opinion (and we’re right about it) that most 3rd party themes are bloated and filled with junk code and slow WP sites down. But they look pretty and that’s what counts? No… no… no.

We could have built a child theme off a very minimalist theme such as Blocksy, or even on top of the new TwentyTwentyOne shipped with the latest WordPress at the time of our build. We opted instead to do what we’re most comfortable doing and what works best on our work with any serious WordPress client site. That’s to start with a fully blank slate. And no, not a theme named “Slate” but an actual blank slate.

Even if you plan to go the way of the masses and adopt some existing theme(s) to work with regularly, we highly recommend every developer should know how themes work and at least once build a fully custom theme just you understand what’s going on under the hood.

Step 1 Create the theme’s directory

We need an empty directory under /wp-content/themes/{theme-name}/ to start. For our project we named the theme simply wpcode so the path is:

/wp-content/themes/wpcode/

Now if you like TDD (test-driven development) then play along and run a simple test of your theme being able to install right now.

how to build a custom WordPress theme from scratch

When you visit WP Admin > Appearance > Themes at the bottom of the page under your installed themes list you should see this, the Broken Themes section. Congrats, you’ve created a broken theme. What I like about this is it actually tells exactly what is wrong.

Step 2) Make the required style.css file. Make this a totally empty file to start, then repeat the activation test by refreshing the Themes page in the WP Admin.

Congratulations, you have now improved your broken theme and have been rewarded with a new error message, Template is missing.

Step 3) Now we will add the required index.php file. Again add merely an empty file to your theme directory. Now do your activation test again by refreshing the Themes page in the WP Admin. You should now see the theme is ready for activation, the broken themes section is gone and your theme is waiting to be activated. Success at long last.

Now I know what you’re wondering, why did we break this down into 3-steps when you could have just said make a directory and put 2 empty files in it named style.css and index.php. Well, I don’t have a good answer for you but I hope it demonstrates exactly how minimal the requirements for a custom theme are in WordPress. And by doing “less” we now have a truly blank slate to work with. This is instead of opting to make a declaration block in our theme with the details of it’s name and license etc, this is instead of diving into a loop in our index.php template. We’ll do those steps later. Just remember in the future whenever you think about what’s involved in custom theme development, it can start with a folder and 2 empty files.

Activate your custom theme and then head to the front of the site and see your WordPress canvas.

Step 4 Let’s get some minimal output rendering from our main index.php template. We’ll add the WordPress functions for rendering a header and footer as well as put some text in between, like so:

<?php get_header(); ?>

<h1>Welcome to WPCode</h1>
<p>You're in the right place to learn to code in WP for free baby!</p>

<?php get_footer(); ?>

Then refresh the front of your site and you should see something like this rendered. Note that this is rendering a default header template and footer template that WordPress provided when your theme does not have one yet.

Key Concept: in WordPress the template hierarchy is used to determine which template file to load. The index template (index.php) is the last or final option, which is why it’s required in any base (stand-alone) theme.

Step 5) Adding a post rendering loop.

This point is a fork in the road as a developer because on any real project you will want to make a custom header and footer for your theme. But right now, the default header/footer combo that ships with WP is working fine, so we’re going to focus on actually rendering content using “the loop”.

I suggest you might want to read up on “the loop” which you can find detailed in the official WordPress docs because it’s an important concept in WP. In short what the loop refers to is iteration over any currently loaded posts. So it has the ability to show either the 1 single post that should be shown like when you’re visiting a single blog post, or it can show a list of posts like on an archive page. Before we use the loop you would find our theme shows the same static content from the index.php file whether we visit the homepage, a blog post page or an archive page. That’s because it lacks any rendering of the actual WP post content, even though WordPress is still loading the post(s) for each page.

<?php get_header(); ?>

<h1>Welcome to WPCode</h1>
<p>You're in the right place to learn to code in WP for free baby!</p>

<h3>---------- LOOP CONTENT BELOW ---------</h3>

<?php
if ( have_posts() ) :
  while ( have_posts() ) : the_post();
    the_title();
    the_content();
  endwhile;
endif;
?>

<?php get_footer(); ?>

When you test for this step make sure you have at least 1 or 2 published test posts with some content so you can see if the content is being rendered.

In our WP Code project this article I’m currently writing showed up in the test because the homepage is automatically showing any posts available. Later I’ll be changing that through the WP settings to show a static home page.

Step 6 Adding a page template

Now we’re starting to build support for different types of content. Remember the mention of template hierarchy earlier? One of the templates that is “more specific” and earlier in the hierarchy than index.php is page.php which is the default page template. Bear in mind that “pages” are separate forms of posts in WP, usually used for static site pages like “home”, “about” and so on.

Start by making the page.php file at the root of your theme and then add the following code.

<?php get_header(); ?>

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

  <h1><?php the_title(); ?></h1>
  <div class="main-content">
    <?php the_content(); ?>
  </div>

<?php
  endwhile;
endif;
?>

<?php get_footer(); ?>

In comparison to the index.php you’ll notice just a few edits. First the static test content is gone. This is fully dynamic, it will show only the current page content plus the header/footer. We’ve broken up the loop into 3 blocks of code. We have the start of the loop using have_posts() and while{}. Then we break out of PHP ending that block, and this allows us to wrap our output in some HTML tags. We wrap the title in an <h1> tag and the content goes inside of a <div>, and we’ve added our first custom CSS class “main-content”. The loop then closes in the PHP block below.

You’ll need to make a page through the WP admin and then visit that page to test if you’re new page template is working. We made an “About Us” page to do this test, and the result looks like this below:

Step 7) Enqueue the theme stylesheet

You might imagine that style.css would be automatically included in your theme because it is a requirement for the theme to have this file. However, WordPress no longer includes it by default, it used to. Now you still have to enqueue the file just like any other CSS file or script. To do this first we’ll make the functions.php file that we need at the root of the theme.

Functions.php does not have to be included, WordPress automatically detects and load this file very early in the loading process. You can use any WP function or “hook” call inside of it.

Now if you’re new to the WP hooks system, you may want to read up on the 2 forms of hooks available which allow developers to run their code at various times. Action hooks in particular are all about timing, whereas filters are about changing (filtering) data that is being used by certain functions. At this point we need an action hook, and we need to use “wp_enqueue_scripts” because this is the hook that fires when WordPress is building the list of scripts to include in the header and footer.

<?php 

add_action('wp_enqueue_scripts', 'wpcodeScripts');

function wpcodeScripts() {

  wp_enqueue_style(
    'wpcode-main-css',
    get_template_directory_uri() . '/style.css',
    [],
    time()
  );
  
}

First examine the add_action() which hooks into ‘wp_enqueue_scripts’. This is used to setup the timing, so that our inclusion of the style.css file will be made when WP is organizing the different scripts for rendering into the header and footer. The second argument is the name of our callback function which we’ve named ‘wpcodeScripts’.

Next we have the wpcodeScripts() callback where we put a call to the wp_enqueue_style() function. This core WP function gives WordPress all the details about our script. It takes up to 5 arguments, but we’ve only used 4. First we specify a unique ID for the script, in this case ‘wpcode-main-css’. Notice how we’re using the theme name (wpcode) as a prefix to make the ID unique, and we did the same thing with the function itself.

The get_template_directory_uri() function returns the URI/URL for the root of our theme, so we only have to add ‘/style.css’ after it to specify the path to our main CSS stylesheet. Note that you do need to include the forward slash as the get_template_directory_uri() will not return the trailing slash.

The 3rd argument for wp_enqueue_style() is an array of dependencies, other stylesheets that our requires. At this point our stylesheet doesn’t have any dependencies so we just pass an empty array [].

Finally the 4th parameter is actually the version number, so you may be puzzled as to why we have time() function there. This is a little trick to avoid the browser caching our stylesheet during development. Later in production you should search/replace time() in all your script calls, and this would normally be the version number as a string such as “1.0.0” or a constant such as THEME_VERSION. Setting it as a constant will make it easier to handle version updates. But for now, time() means a fresh reload of our changes making testing 5X less frustrating. Nothing worse than a cached stylesheet when you’re trying to do CSS changes.

Now when you refresh the front of your site and check the source code, you should be able to search for style.css and see it included. You can also put some CSS styles into the sheet for testing purposes as well, something like body { background: #000; } will give you a clear indicator to whether the stylesheet is loaded successfully.

Step 8) Add reset CSS to normalize element styles

When you build a theme entirely from scratch you may find the initial style of elements surprising. And there are some defaults, like list styles that will have to continually be changed as we start to build the UX. A CSS reset tool can help us here, and I’ll recommend the Reset CSS project from meyerweb.com for this. Simply copy the code from https://meyerweb.com/eric/tools/css/reset/ into your style.css.

Step 9) Add a custom header

Now it’s time to replace the default header that WordPress is loading with one of our own. Start by making a header.php file at the root of your theme. If you want to test the result, you’ll notice looking at the front-end of the site that now there simply is no header. You’re empty file is now being loaded.

There are some requirements for the header. In WordPress only the code you choose to render reaches the browser, so if you don’t include a doctype tag and other required HTML5 sections, then you won’t end up with valid HTML5.

Below is a very streamlined example of a minimalist header. It has the required HTML5 tags and sections, and it has some recommended WordPress function calls such as language_attributes(). We won’t go into what each of these does now, but if you want to know the WP Codex can give you an understanding of what each of these does.

<!DOCTYPE html>

<html class="no-js" <?php language_attributes(); ?>>

	<head>

		<meta charset="<?php bloginfo( 'charset' ); ?>">
		<meta name="viewport" content="width=device-width, initial-scale=1.0" >

		<link rel="profile" href="https://gmpg.org/xfn/11">

		<?php wp_head(); ?>

	</head>

	<body <?php body_class(); ?>>

Notice our header ends with the opening body tag, so at this point the header itself does not render anything. What we’re going to do for now is add a very slim minimalist header with a bit of CSS.

This part goes into the header.php file under the body open tag:

<header class="site-header">
  <div>WPCODE</div>
</header>

Then a bit of CSS is placed into style.css:

/* Header Styles */
header.site-header {
  background: #484848;
  font-size: 3.0em;
  color: #FFF;
  padding: 20px;
}

This was result. It may not win design awards but we’re keeping it nice and simple here and that lets us get our theme structured rapidly and then we’ll iterate over later with more focus on styling. The header of course will later need a menu added, but that will be easier to handle now that we already have loaded a custom header.

Now the only part of the page auto-generated is the footer, in the next step we’ll change that.

Step 9) Add a custom footer

Very similar process here to creating the custom header. We start with a footer.php file placed at the root of the theme directory. Below is our starting point for the file:

<footer></footer>

<?php wp_footer(); ?>

</body>
</html>

The wp_footer() call is very important, without that scripts setup to load in the footer won’t be loaded and that includes the WP admin bar. We also need to close the <body> and <html> tags that were opened in the header.

The <footer> tag is where we’ll actually put the output from our footer, and as with the header we’ll quickly draft a basic starting point.

Update the footer.php with our <footer> tag content:

<footer class="site-footer">
  <div class="text-logo">WPCODE</div>
  <div class="copyright">&copy; 2020 WPCODE</div>
</footer>

<?php wp_footer(); ?>

</body>
</html>

Next sprinkle a little CSS on top placing this snippet into the style.css:

/* Footer Styles */

footer.site-footer {
  background: #676767;
  font-size: 1.5em;
  color: #FFF;
  padding: 15px;
}

footer.site-footer .copyright {
  font-size: 0.8em;
}

The result on the front-end should now look something like this:

And now, congratulations because 100% of the output to the front-end is coming from the template and it’s custom header and footer includes. You are for the most part, in complete control of the rendering of the site. And that’s exactly why building custom themes is a powerful and liberating way of approaching WordPress development. Let’s continue onward shall we?


Are you enjoying how to build a custom WordPress theme from scratch so far?

If you’re enjoying this content so far please consider taking a moment to share it on social media or leave us a comment below. These actions help us reach more great folks like yourself and also to keep the content flowing and free flowing here at WPCODE. Thanks!

Step 10) A few rapid fire dashes of CSS to spruce things up a bit


About the Instructor

Casey Milne

Senior WordPress Developer, Eat/Build/Play

Casey started programming PHP in 2003. His early work involved PHP application development and custom sites built from PHP and HTML. He adopted Drupal in the early days of CMS, and then switched later to WordPress in 2012. For the past 8-years he has developed WordPress plugins and large-scale WP sites. Casey's main forte is handling API integrations and building data-driven functionality in WordPress.