In this article I'm going to show you my personal and opinionated WordPress theme development workflow. The goal of this article is to highlight the power of the WordPress Command Line when combined with automated build tools, along with an organized project structure.

0. Install the WordPress Command Line

The WordPress Command Line is a Command line interface for WordPress. Simply put, this means that it can automate many tasks for us. One task is generating a starter theme. However, it can do much, much more.

1. Use the WordPress Command Line To Generate a New Starter Theme

Once you've installed the CLI, follow these steps to generate a base theme.

In the root of your WordPress install, run wp scaffold _s slug_for_your_theme --activate . For the case of this tutorial, I will run wp scaffold _s demo-theme --activate . The wp scaffold _s function generates starter code for a theme based on _s (Underscores) .

. The Underscores Theme is created by Automatic, which is the same company that brings us WordPress. By default, it generates all files and directories needed for a valid WordPress theme.

Note that you can manually download the Underscores Theme, but using the CLI is much more effective.

If you navigate to the front end of your website, you should notice an underwhelming theme.

As underwhelming as it is, this starter theme meets all of WordPress's theme development standards. In short, this means that all the necessary templates, functions and css are generated for you.

If you were to run your theme against the Theme Check plugin and its 6,108 tests, you'll only get 2 warnings. One is about a hidden file that is generated, and the other is about changing the Theme URI and Author URI. However, these only matter if you plan on publishing your theme to WordPress.

I like to run my theme against Theme Check throughout the development process to ensure any changes I make still meet WordPress's theme development standards.

At this point you could start editing the style.css and template files to build your custom theme. However, it's much more effective to use modern build tools to speed up development.

2. Install and Configure WPGulp

Now that we have a base theme configured, we'll want it to be customized with our own CSS, JS and template files. Using a build tool like WPGulp helps speed up this process.

WPGulp is an advanced & extensively documented Gulp.js + WordPress workflow. It can help you kick-start a build-workflow for your WordPress plugins and themes with Gulp.js, save you a lot of grunt work time, follow the DRY (Don't Repeat Yourself) principle, and #0CJS Zero-config JavaScript startup but still configurable via wpgulp.config.js file...

You can read about all of what WPGulp does, but some of my favorite features are...

Hot reloading

ES6 compiling

SASS compiling

Automatic image minification

Compiles all JS and CSS into one file each

Follow the docs to install WPGulp. In my case I cd into my theme by running cd wp-content/themes/demo-theme/ . I then run npx wpgulp .

Once installation is complete, you'll want to edit the wpgulp.config.js file. Change the projectURL: 'wpgulp.local' variable to match your local development URL. If you plan on translating your site, make sure to update the translation options .



Now that WPGulp is installed and configured, we'll want to update our theme's file structure to match the recommendations in the wpgulp.config.js . These updates are based on the styleSRC , jsVendorSRC , jsCustomSRC and imgSRC variables.

Assuming you're still in your theme's directory, run the following commands. mkdir -p assets/css assets/js/vendor assets/js/custom assets/img/raw Now that the file structure matches the wpgulp.config.js configuration, we'll want to move existing .css and .js files by running the following commands. mv style.css assets/css/style.scss This moves the theme's style.css file into the assets/css , and also changes it to a .scss file. mv layouts/ assets/css/layouts This moves the theme's generated layout directory into assets/css/ mv assets/css/layouts/sidebar-content.css assets/css/layouts/sidebar-content.scss mv assets/css/layouts/content-sidebar.css assets/css/layouts/content-sidebar.scss These commands change the .css files into .scss files.

files into files. Note that these are boilerplate layout files generated by the wp scaffold _s command, and that they were never loaded into the theme in the first place. If you want to use one of them, you'll need to import it into the styles.scss file. I personally never use them. mv js/customizer.js assets/js/custom/customizer.js mv js/navigation.js assets/js/custom/navigation.js mv js/skip-link-focus-fix.js assets/js/custom/skip-link-focus-fix.js rm -R js/ These commands simply move existing javascript files generated by the wp scaffold _s command into the new javascript directory.

At this point your assets directory should look like this.

Now that the assets directory is configured, we'll need to update the theme's functions.php file.

Open up your theme's functions.php file and scroll down to the Enqueue scripts and styles. section. It should look something like this.

function demo_theme_scripts ( ) { wp_enqueue_style ( 'demo-theme-style' , get_stylesheet_uri ( ) ) ; wp_enqueue_script ( 'demo-theme-navigation' , get_template_directory_uri ( ) . '/js/navigation.js' , array ( ) , '20151215' , true ) ; wp_enqueue_script ( 'demo-theme-skip-link-focus-fix' , get_template_directory_uri ( ) . '/js/skip-link-focus-fix.js' , array ( ) , '20151215' , true ) ; if ( is_singular ( ) && comments_open ( ) && get_option ( 'thread_comments' ) ) { wp_enqueue_script ( 'comment-reply' ) ; } } add_action ( 'wp_enqueue_scripts' , 'demo_theme_scripts' ) ;

WPGulp will concatenate all javascript files in the assets/js/custom and assets/js/vendor directories into one file each. This means that we don't need to individually load navigation.js and skip-link-focus-fix.js anymore. Note that these files were generated by the wp scaffold _s command, and aren't required for every WordPress theme.

Remove the existing wp_enqueue_script functions and replace with the following. I've commented out the vendor scripts, since we currently have none. I just wanted to highlight how you would load them.

I added 'customize-preview' as an argument to the wp_enqueue_script function for the custom-scripts. This is because the /assets/js/custom/customizer.js file generated by the wp scaffold _s is only loaded on the theme customizer page.

as an argument to the function for the custom-scripts. This is because the file generated by the is only loaded on the theme customizer page. I chose to load the .min versions of each file, but you can load the unminified versions if you wish.

versions of each file, but you can load the unminified versions if you wish. The vendor.min.js and custom.min.js files will be generated once we run WPGulp.

function demo_theme_scripts ( ) { wp_enqueue_style ( 'demo-theme-style' , get_stylesheet_uri ( ) ) ; wp_enqueue_script ( 'demo-theme-custom-scripts' , get_template_directory_uri ( ) . '/assets/js/custom.min.js' , array ( 'customize-preview' ) , '20151215' , true ) ; if ( is_singular ( ) && comments_open ( ) && get_option ( 'thread_comments' ) ) { wp_enqueue_script ( 'comment-reply' ) ; } } add_action ( 'wp_enqueue_scripts' , 'demo_theme_scripts' ) ;

Run npm start to make sure everything is working. You should be able to open up http://localhost:3000/ and see your site. To ensure everything is hooked up, I temporarily added * { border: 1px solid red; } to /assets/css/style.scss . You should see the new styles load instantly in the browser.

I also recommend checking the sources and console tabs on Chrome's developer tools to ensure there are no errors, and that all files are being loaded.

At this point you have everything you need to start creating a custom WordPress theme using modern developer tools. In the next sections I will show you my opinionated setup.

5. Create a CSS Architecture

This next step details my opinionated set up when it comes to my CSS Architecture. I subscribe to the SMACSS way of architecting my CSS, along with the BEM naming convention. This may seem like an obsolete way of doing things if you're coming from a frontend framework or library like React, Vue, Angular etc. However, WordPress isn't a frontend framework, so paradigms like CSS Modules or Styled Components don't apply.

At the end of the day, we're compiling many .scss files into one .css file. Using SMACSS is an excellent way to stay organized and efficient.

Assuming you're in your theme's directory, run the following commands: mkdir -p assets/css/base assets/css/layouts assets/css/module This follows the SMACSS way of architecting CSS. I usually just create these three directories, and don't use state or theme rules. Instead, I create many partials in the modules directory, and use the BEM naming convention. touch assets/css/base/_base.scss assets/css/base/_var.scss This creates two new partials. One for storing SASS variables, and the other for base styles. Copy everything from /assets/css/style.scss and into /assets/css/base/_base.scss Remove everything from /assets/css/style.scss and replace with the following:

@import './base/var' ; @import './base/base' ;

If you run npm start you should notice no changes. This is because we didn't change any css, but instead broke it into partials. From here on out you'll import your partials into style.scss .

6. Add a Typography System

Now that we've set up an architecture for our CSS, I like to add a typography system to the theme. I use typebase.css because of its simplicity, and use of SASS variables.

Assuming you're in your theme's directory, run the following commands: touch assets/css/base/_typography.scss Open /assets/css/style.scss and look for the Typography section. It should start look something like this:

body, button, input, select, optgroup, textarea { color : #404040 ; font-family : sans-serif ; font-size : 16px ; font-size : 1rem ; line-height : 1.5 ; } h1, h2, h3, h4, h5, h6 { clear : both ; } ...

Remove the typography css from style.scss . For me this is between lines 389 and 455 The reason we remove the default typography styles provided by the Underscores Theme is because we will be using typebase.css instead. Open up _typography.scss and paste in the contents from the typebase.scss file. However, don't paste in the Typesetting variables. We will place these in the assets/css/base/_var.scss file instead.

html { font-family : serif ; font-size : $baseFontSize / 16 * 100% ; -webkit-font-smoothing : antialiased ; } p { line-height : $leading ; margin-top : $leading ; margin-bottom : 0 ; } ul, ol { margin-top : $leading ; margin-bottom : $leading ; li { line-height : $leading ; } ul, ol { margin-top : 0 ; margin-bottom : 0 ; } } blockquote { line-height : $leading ; margin-top : $leading ; margin-bottom : $leading ; } h1, h2, h3, h4, h5, h6 { font-family : sans-serif ; margin-top : $leading ; margin-bottom : 0 ; line-height : $leading ; } h1 { font-size : 3 * $scale * 1rem ; line-height : 3 * $leading ; margin-top : 2 * $leading ; } h2 { font-size : 2 * $scale * 1rem ; line-height : 2 * $leading ; margin-top : 2 * $leading ; } h3 { font-size : 1 * $scale * 1rem ; } h4 { font-size : $scale / 2 * 1rem ; } h5 { font-size : $scale / 3 * 1rem ; } h6 { font-size : $scale / 4 * 1rem ; } table { margin-top : $leading ; border-spacing : 0px ; border-collapse : collapse ; } td, th { padding : 0 ; line-height : $baseLineHeight * $baseFontSize - 0px ; } code { vertical-align : bottom ; } .lead { font-size : $scale * 1rem ; } .hug { margin-top : 0 ; }

Open up assets/css/base/_var.scss and paste in the Typesetting variables from typebase.scss file].

$baseFontSize : 22 !default ; $baseLineHeight : 1.5 !default ; $leading : $baseLineHeight * 1rem !default ; $scale : 1.414 !default ;

Finally, import /assets/css/base/_typograhpy.scss into /assets/css/style.scss

@import './base/var' ; @import './base/base' ; @import './base/typography' ;

If you run npm start and open your browser, you should see something like this.

The default size is usually too big for me, so I set $baseFontSize:22 !default; to 16 instead.

7. Add a Grid System

Next I like to add a grid system to my theme. Some argue that this is unnecessary because of CSS Grid, but I've found that having a simple grid system is very helpful.

I highly recommend using Semantic UI Container and Semantic UI Grid because of its easy naming conventions and use of flex-box instead of floats.

First, we need to remove the default layout files that were generated and updated in steps 3.2.2 through 3.2.4. Assuming you're still in your theme's directory, run rm assets/css/layouts/sidebar-content.scss assets/css/layouts/content-sidebar.scss . Run touch assets/css/layouts/_container.scss . Copy the contents from container.css and paste into /assets/css/layouts/_container.scss . Run touch assets/css/layouts/_grid.scss . Copy the contents from grid.css and paste into /assets/css/layouts/_grid.scss . Open up /assets/css/base/_var.scss and add the following SASS variables: These are the breakpoints defined in the Semantic UI Container. Adding these breakpoints as variables is useful when writing custom media queries for your theme.

$baseFontSize : 16 !default ; $baseLineHeight : 1.5 !default ; $leading : $baseLineHeight * 1rem !default ; $scale : 1.414 !default ; $bp--sm : 768px ; $bp--md : 992px ; $bp--lg : 1200px ;

Finally, import assets/css/layouts/_container.scss and assets/css/layouts/_grid.scss into /assets/css/style.scss .

@import './base/var' ; @import './base/base' ; @import './base/typography' ; @import './layouts/container' ; @import './layouts/grid' ;

As a test, add .ui.container classes to the #masthead and #content in wp-content/themes/demo-theme/header.php .

<?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 = " https://gmpg.org/xfn/11 " > <?php wp_head ( ) ; ?> </ head > < body <?php body_class ( ) ; ?> > < div id = " page " class = " site " > < a class = " skip-link screen-reader-text " href = " #content " > <?php esc_html_e ( 'Skip to content' , 'demo-theme' ) ; ?> </ a > < header id = " masthead " class = " site-header ui container " > < div class = " site-branding " > <?php the_custom_logo ( ) ; if ( is_front_page ( ) && is_home ( ) ) : ?> < h1 class = " site-title " > < a href = " <?php echo esc_url ( home_url ( '/' ) ) ; ?> " rel = " home " > <?php bloginfo ( 'name' ) ; ?> </ a > </ h1 > <?php else : ?> < p class = " site-title " > < a href = " <?php echo esc_url ( home_url ( '/' ) ) ; ?> " rel = " home " > <?php bloginfo ( 'name' ) ; ?> </ a > </ p > <?php endif ; $demo_theme_description = get_bloginfo ( 'description' , 'display' ) ; if ( $demo_theme_description || is_customize_preview ( ) ) : ?> < p class = " site-description " > <?php echo $demo_theme_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 ( 'Primary Menu' , 'demo-theme' ) ; ?> </ button > <?php wp_nav_menu ( array ( 'theme_location' = > 'menu-1' , 'menu_id' = > 'primary-menu' , ) ) ; ?> </ nav > </ header > < div id = " content " class = " site-content ui container " >

The content should now have a max width

Conclusion and Next Steps

Regardless of what the design looks like, I start every custom WordPress theme with the above steps. From there I add color variables, and begin to style the header and footer first. I always populate the site with dummy data so that I can style each unique page template, and account for edge cases.