Ready to build your own WordPress theme from scratch, with all the bells and whistles? This guide helps you get started from the very beginning with step-by-step instructions and helpful tips. In this first section, we’ll create the files for the new theme, set up its basic styles and create a header and a footer.

Do you have a WordPress development environment ready on your computer? If not, follow this easy step-by-step guide to setup IIS, PHP, MySQL and WordPress, all on your Windows computer.

Creating an empty theme

In order to create a new, empty theme, find the wp_content/themes directory. In it, create a fresh subdirectory with the name of your theme, e.g. mytheme.

A theme needs at least the following files, which you should now create:

  • index.php
  • functions.php
  • style.css

Inside the style.css file, you should put your theme information:

/*
Theme Name: MyTheme
Theme URI: http://www.independent-software.com
Author: Independent Software
Author URI: http://www.independent-software.com
Description: WordPress theme development guide
Version: 1.0
License: Private
Text Domain: mytheme
*/

Inside index.php, simply write “hello, world” to see that it works. The functions.php file can be left empty for now.

Inside the WordPress administration panel, navigate to Appearance, then Themes. Your new theme should be in the list of themes shown. If you’re missing any of the files above, WordPress will warn you about this. Activate the new theme, then refresh your WordPress website. If all is well, you should see “hello world” on a white background.

In order to save disk space, it is now safe to delete the other themes’ subdirectories (twentyfifteen, twentysixteen and twentyseventeen). You’ll save about 3MB.

First HTML

Let’s elaborate on the HTML in the index.php file a little. Change it to:

<html>
  <head>
    <?php wp_head(); ?>
  </head>
  <body>
    <h1>My Title</h1>
    <h2>Header 2</h2>
    <p>Some text here.</p>
  </body>
</html>

This is standard HTML that includes a couple of elements that will allow us to fiddle with the theme’s CSS in a moment. Speaking of CSS, note that there’s a special bit of PHP in there: wp_head. This tells WordPress to insert its <head> information. This includes references to stylesheets, fonts and JavaScript code.

Let’s try that out. Add a new CSS rule to style.css:

h1 {
  color: red;
}

…and refresh the page. Unfortunately, nothing changes.

Adding the stylesheet

Should we place a direct reference to style.css in the <head> section? No! Key to working with WordPress is letting WordPress manage all files and content, and write only code that focuses on presentation. If you add a reference to a stylesheet directly, WordPress will have no knowledge about it and cannot enqueue the link properly to play nice with other stylesheets, fonts and JavaScript sources.

Instead, add the following to functions.php:

function mytheme_scripts() {
  // Theme stylesheet.
  wp_enqueue_style( 'mytheme-style', get_stylesheet_uri() );
}
add_action( 'wp_enqueue_scripts', 'mytheme_scripts' );

Most things in WordPress are defined as “actions” or “hooks” in functions.php. It’s not a big thing to worry about now, but you’ll see a lot of this as we keep adding code to functions.php. This approach allows WordPress to execute your code at the precise moment is needs it in the page rendering cycle.

WordPress offers a long list of “actions”. One of these is wp_enqueue_scripts and it executes the function you provide when it’s time to, you guessed it, enqueue scripts. In this code, then, we add the action wp_enqueue_scripts and point it to our custom mytheme_scripts function.

Note: I could have called my function “scripts”, but that might clash with other PHP code. For this reason, it is customary to prefix all functions you write in functions.php with the name of your theme.

Inside the custom function, we tell WordPress to enqueue our stylesheet using the wp_enqueue_style function provided by WordPress. The get_stylesheet_uri function, meanwhile, returns the full path to style.css.

You will gather from this that WordPress development revolves around knowledge of the WordPress API, and knowing which function to call where. Such is WordPress life. On the plus side, once you get the basics done you’ll quickly get the hang of it.

Refreshing the page, you will now see that the h1 element turns red as expected. If you view the source of the page, you will see that WordPress has included our stylesheet, as well as stylesheets for the admin bar and dashboard icons.

Let’s get started on proper CSS for our theme by normalizing the browser style rules, so that we start with a clean slate whichever browser we use. This can be done by including normalization CSS. It’s as simple as including this in style.css:

	
html {
  font-family: sans-serif;
  line-height: 1.15;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}
 
body {
  margin: 0;
}
 
article, aside, footer,header, nav, section {
  display: block;
}
 
h1 {
  font-size: 2em;
  margin: 0.67em 0;
}
 
figcaption, figure, main {
  display: block;
}
 
figure {
  margin: 1em 0;
}
 
hr {
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  height: 0;
  overflow: visible;
}
 
pre {
  font-family: monospace, monospace;
  font-size: 1em;
}
 
a {
  background-color: transparent;
  -webkit-text-decoration-skip: objects;
}
 
a:active, a:hover {
  outline-width: 0;
}
 
abbr[title] {
  border-bottom: 1px #767676 dotted;
  text-decoration: none;
}
 
b, strong {
  font-weight: inherit;
  font-weight: 700;
}
 
code, kbd, samp {
  font-family: monospace, monospace;
  font-size: 1em;
}
 
dfn {
  font-style: italic;
}
 
mark {
  background-color: #eee;
  color: #222;
}
 
small {
  font-size: 80%;
}
 
sub, sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}
 
sub {
  bottom: -0.25em;
}
 
sup {
  top: -0.5em;
}
 
audio, video {
  display: inline-block;
}
 
audio:not([controls]) {
  display: none;
  height: 0;
}
 
img {
  border-style: none;
}
 
svg:not(:root) {
  overflow: hidden;
}
 
button, input, optgroup, select, textarea {
  font-family: sans-serif;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
 
button, input {
  overflow: visible;
}
 
button, select {
  text-transform: none;
}
 
button, html [type="button"], [type="reset"], [type="submit"] {
  -webkit-appearance: button;
}
 
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}
 
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}
 
fieldset {
  border: 1px solid #bbb;
  margin: 0 2px;
  padding: 0.35em 0.625em 0.75em;
}
 
legend {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  color: inherit;
  display: table;
  max-width: 100%;
  padding: 0;
  white-space: normal;
}
 
progress {
  display: inline-block;
  vertical-align: baseline;
}
 
textarea {
  overflow: auto;
}
 
[type="checkbox"],
[type="radio"] {
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  padding: 0;
}
 
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}
 
[type="search"] {
  -webkit-appearance: textfield;
  outline-offset: -2px;
}
 
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}
 
::-webkit-file-upload-button {
  -webkit-appearance: button;
  font: inherit;
}
 
details, menu {
  display: block;
}
 
summary {
  display: list-item;
}
 
canvas {
  display: inline-block;
}
 
template {
  display: none;
}
 
[hidden] {
  display: none;
}

After this, you should see your text in the browser change from Times New Roman to a more palatable sans-serif.

While we’re at it, we’ll also add CSS for accessibility (dealing with text intended for screen readers). We can use the CSS included in WordPress’s own twentyseventeen theme CSS:

.screen-reader-text {
  clip: rect(1px, 1px, 1px, 1px);
  height: 1px;
  overflow: hidden;
  position: absolute !important;
  width: 1px;
  word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */
}
 
.screen-reader-text:focus {
  background-color: #f1f1f1;
  -webkit-border-radius: 3px;
  border-radius: 3px;
  -webkit-box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
  box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
  clip: auto !important;
  color: #21759b;
  display: block;
  font-size: 14px;
  font-size: 0.875rem;
  font-weight: 700;
  height: auto;
  left: 5px;
  line-height: normal;
  padding: 15px 23px 14px;
  text-decoration: none;
  top: 5px;
  width: auto;
  z-index: 100000; /* Above WP toolbar. */
}

Apart from this generic CSS, WordPress also expects some style rules to exist. For instance, rules for aligning content to the left, center or right is used by WordPress in its editor for posts and pages. These rules are:

.alignleft{
  display: inline;
  float: left;
  margin-right: 1.5em;
}
 
.alignright {
  display: inline;
  float: right;
  margin-left: 1.5em;
}
 
.aligncenter {
  clear: both;
  display: block;
  margin-left: auto;
  margin-right: auto;
}

You might define these rules differently, but it is important they exist as WordPress will generate HTML that uses them.

We’ve now added quite a bit of CSS. It is important to realize that this CSS should be included in the <head> of every page of our website, and so far we are calling wp_head (which will insert the CSS reference for us) only in index.php. What if we also have contact.php and news.php? Should we write the header code again for each page?

It will not come as a total surprise that the answer is no. WordPress expects us to do the obvious thing and create separate header and footer files, which will include in our pages.

Our index.php file will be reduced to this:

<div class="wrap">
  <h1>My Title</h1>
  <h2>Header 2</h2>
  <p>Some text here.</p>
</div>
<?php get_footer(); ?>

What we had in index.phpmoves to header.php, which is the file WordPress will look for when we call get_header:

	
<!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">
  <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
  <div class="site-content-contain">
    <div id="content" class="site-content">

We’ve added a couple of new WordPress goodies here:

body_class will automatically add class name for pages vs posts, user logged in or not logged in and page ID, which can be useful if your styles must change based on these properties. The other classes used here must be defined in our style.css and are not WordPress-specific.

For the footer, we’ll create footer.php (which, again, is the default file WordPress will look for when we call get_footer):

      </div><!-- #content -->
      <footer role="contentinfo">
        ...
      </footer><!-- #colophon -->
    </div><!-- .site-content-contain -->
  </div><!-- #page -->
  <?php wp_footer(); ?>
  </body>
</html>

As with wp_head, we call wp_footer here to allow WordPress to insert footer-specific things, like JavaScripts that must be placed just before the closing </body> element. If you don’t add this, WordPress won’t have a chance to add these things.

With all this done, refresh your page and you’ll see the WordPress administration bar appearing at the top (provided you’re logged into the administration panel as well).

Summary

So far, we have:

  • created required theme files index.php, functions.php and style.css
  • added theme information to style.css
  • added some normalization CSS and a couple of WordPress required rules
  • split the header and footer code off into separate files.

In the next section of this guide, we’ll look at adding WordPress content to our theme:

Continue to Content & the WordPress Template Hierarchy