How to Create a Custom WordPress Theme with Tailwind CSS (Step-by-Step Guide)

How to Create a Custom WordPress Theme with Tailwind CSS

In this guide we’ll build a complete custom WordPress theme with Tailwind CSS, from an empty folder to a polished, production-ready theme – a homepage with hero and services sections, a blog with cards and pagination, single posts with comments, page templates, a 404, the lot. This isn’t a “hello world” exercise: it’s the Business Tailwind theme we ship, and you can download the finished theme free at the end to follow along or use as your starter.

The finished theme's homepage

Why Tailwind CSS for WordPress Themes?

Tailwind is a utility-first CSS framework: you style elements by composing small classes (flex, py-20, text-ink) directly in your templates, and a build step generates a stylesheet containing only the classes you actually used. For WordPress theme developers that means:

  1. A tiny stylesheet. Our finished theme ships 27 KB of minified CSS – most hand-written theme stylesheets pass 150 KB and keep growing. Less CSS means faster First Contentful Paint and better Core Web Vitals, which Google rewards.
  2. The design lives in your templates. When a template and its styles are the same file, nothing gets orphaned. Delete a template, its styles vanish from the next build automatically.
  3. A design system for free. Colors, fonts and spacing live in tailwind.config.js, so every template stays consistent without a style guide document nobody reads.

What You’ll Need

  • A local WordPress install (LocalWP, DevKinsta, or wp-env all work)
  • Node.js 18+ with pnpm or npm
  • Any code editor

Step 1: Create the Theme Folder and Required Files

A WordPress theme needs exactly two files to exist: style.css with a comment header, and index.php. Create your folder inside wp-content/themes/:

cd wp-content/themes
mkdir business-tailwind && cd business-tailwind

Here’s the full structure we’re going to end up with – worth creating the folders now:

Theme folder structure

style.css carries the theme header WordPress reads for the Appearance → Themes screen. The actual design will live in a compiled Tailwind file, so this stays almost empty:

/*
    Theme Name: Business Tailwind
    Author: Bytes Brothers
    Description: A multipurpose corporate WordPress theme built with Tailwind CSS.
    Version: 1.0.0
    Requires PHP: 7.4
    License: GNU General Public License v2 or later
    Text Domain: business-tailwind
*/

Add an empty index.php for now, and your theme already appears in the admin. Activate it – we’ll build it up live.

Step 2: Set Up Tailwind Inside the Theme

Tailwind runs as a dev dependency inside the theme folder. Install it (npm users: swap pnpm add for npm install):

pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Installing Tailwind with pnpm

The single most important line in a WordPress + Tailwind setup is the content array: point it at your PHP templates, so Tailwind finds every class you use in them:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './*.php',
    './includes/**/*.php',
    './loops/**/*.php',
    './assets/js/*.js',
  ],
  theme: {
    extend: {
      colors: {
        primary: { DEFAULT: '#ef4135', 50: '#fef2f2', /* … */ 600: '#dc2a1e' },
        ink: { DEFAULT: '#101828', soft: '#475467', mute: '#98a2b3' },
      },
      fontFamily: {
        sans: ['Poppins', 'ui-sans-serif', 'system-ui', 'sans-serif'],
        slab: ['"Roboto Slab"', 'ui-serif', 'serif'],
      },
    },
  },
}

Everything in theme.extend is your design system: one primary brand palette, an ink neutral scale for text, and two font families.

Create src/input.css with the three Tailwind directives, plus a small @layer components block for patterns you’ll reuse in many templates (buttons, cards, section titles). Keep this layer small – utilities in the markup are the default; components are the exception:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .container-site {
    @apply mx-auto w-full max-w-site px-4 sm:px-6 lg:px-8;
  }
  .btn-primary {
    @apply inline-flex items-center justify-center gap-2 rounded-md
           bg-primary px-6 py-3 text-sm font-medium uppercase
           tracking-wider text-white transition-colors hover:bg-primary-600;
  }
  .section-title {
    @apply mb-10 text-3xl font-semibold text-ink sm:text-4xl;
  }
  .card {
    @apply rounded-lg bg-white shadow-sm ring-1 ring-slate-100
           transition-shadow duration-300 hover:shadow-lg;
  }
}

Wire up the build scripts in package.json:

quot;scripts": {
  "dev":   "tailwindcss -i ./src/input.css -o ./assets/css/business.css --watch",
  "build": "tailwindcss -i ./src/input.css -o ./assets/css/business.min.css --minify"
}

Run pnpm run dev in a terminal and leave it running – every template you write from here on rebuilds the stylesheet instantly.

The production build - 27 KB of CSS

Step 3: Enqueue the Stylesheet the WordPress Way

Never hardcode a <link> tag – enqueue. We keep enqueues in their own include so functions.php stays a clean bootstrap. Create includes/bt-styles-scripts.php:

add_action('wp_enqueue_scripts', 'bt_styles');
function bt_styles() {
    wp_enqueue_style('bt-fonts',
        'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Roboto+Slab:wght@300;400;700&display=swap',
        array(), null);

    // Compiled Tailwind stylesheet - built from src/input.css.
    wp_enqueue_style('bt-tailwind',
        BT_ASSETS_DIRECTORY_URI . 'css/business.min.css', array(), BT_THEME_VERSION);

    wp_enqueue_style('bt-style', get_stylesheet_uri(), array('bt-tailwind'), BT_THEME_VERSION);
}

Use a version constant, not time() – you want browsers to cache the file, and you bump the constant when you rebuild.

Then functions.php bootstraps everything – includes, theme supports, menus and image sizes:

define('BT_THEME_VERSION', '1.0.0');

$bt_inc = get_template_directory() . '/includes/';
require_once $bt_inc . 'constants.php';            // TEMPLATE_DIRECTORY_URI etc.
require_once $bt_inc . 'bt-styles-scripts.php';    // enqueues
require_once $bt_inc . 'template-functions.php';   // banner + brand helpers

add_action('after_setup_theme', 'bt_theme_setup');
function bt_theme_setup() {
    add_theme_support('title-tag');
    add_theme_support('post-thumbnails');
    add_theme_support('custom-logo');
    add_theme_support('html5', array('search-form', 'comment-form', 'comment-list'));

    register_nav_menus(array(
        'primary'      => __('Primary Menu (header)', 'business-tailwind'),
        'footer_quick' => __('Footer - Quick Links', 'business-tailwind'),
    ));

    add_image_size('card-thumb', 800, 450, true);    // blog cards (16:9)
    add_image_size('banner-wide', 1600, 700, true);  // page banners
}

Step 4: Build header.php and footer.php

Every page template starts with get_header() and ends with get_footer(). The header is where Tailwind shines – a sticky, blurred white bar is a handful of utilities:

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php wp_head(); ?>
</head>
<body <?php body_class('bg-white font-sans text-ink-soft antialiased'); ?>>
<?php wp_body_open(); ?>

<header id="site-header" class="sticky top-0 z-50 bg-white/95 backdrop-blur transition-shadow">
  <div class="container-site">
    <nav class="flex flex-wrap items-center justify-between py-4 lg:py-0">
      <?php echo bt_brand('header'); ?>
      <button id="nav-toggle" class="rounded-md p-2 text-ink ring-1 ring-slate-200 lg:hidden"
              aria-controls="nav-menu" aria-expanded="false">☰</button>
      <div id="nav-menu" class="hidden w-full pt-4 lg:block lg:w-auto lg:pt-0">
        <?php wp_nav_menu(array(
            'theme_location' => 'primary',
            'menu_class'     => 'main-nav',
            'container'      => false,
        )); ?>
      </div>
    </nav>
  </div>
</header>
<main>

The navigation is a standard wp_nav_menu() – editors manage it from Appearance → Menus. The trick is styling WordPress’s own menu classes once, in src/input.css, so dropdowns and active states work for whatever menu the editor builds:

.main-nav { @apply flex flex-col lg:flex-row lg:items-center; }
.main-nav > li > a { @apply nav-link; }
.main-nav > li.current-menu-item > a { @apply text-primary; }
.main-nav ul.sub-menu {
  @apply hidden lg:invisible lg:absolute lg:top-full lg:z-50 lg:block lg:w-52
         lg:rounded-b-md lg:bg-white lg:py-2 lg:opacity-0 lg:shadow-lg
         lg:ring-1 lg:ring-slate-100 lg:transition-opacity;
}
.main-nav li:hover > ul.sub-menu,
.main-nav li:focus-within > ul.sub-menu { @apply block lg:visible lg:opacity-100; }

footer.php closes </main>, renders the widget columns (a footer_quick menu location plus a recent-posts query) and – critically – calls wp_footer() before </body>.

Step 5: The Template Hierarchy

WordPress picks templates by name. Our theme covers the hierarchy like this:

Template Renders
index.php Blog index (and final fallback)
archive.php Category, tag, author, date archives
single.php + comments.php Single posts
page.php Static pages
front.tpl.php Homepage (assignable page template)
coming-soon.tpl.php, 404.tpl.php Special pages

Repeated markup goes into partials. Our blog card lives in loops/blog-card.php and gets reused by the blog index, archives and the homepage:

<article <?php post_class('card overflow-hidden'); ?>>
  <a href="<?php the_permalink(); ?>" class="group block overflow-hidden">
    <?php the_post_thumbnail('card-thumb', array(
        'class'   => 'aspect-[16/8] w-full object-cover transition-transform
                      duration-500 group-hover:scale-105',
        'loading' => 'lazy',
    )); ?>
  </a>
  <div class="p-7">
    <h2 class="mb-3 text-xl font-semibold leading-snug">
      <a href="<?php the_permalink(); ?>" class="hover:text-primary"><?php the_title(); ?></a>
    </h2>
    <p class="mb-4 text-sm leading-relaxed">
      <?php echo esc_html(wp_trim_words(get_the_excerpt(), 24)); ?></p>
  </div>
</article>

index.php is then just a banner, a grid, and the loop:

get_header();
echo bt_render_banner(array('title' => 'Blog', 'page_name' => 'Blog'));
?>
<section class="py-20 lg:py-24">
  <div class="container-site">
    <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
      <?php while (have_posts()) : the_post();
          get_template_part('loops/blog-card');
      endwhile; ?>
    </div>
    <?php the_posts_pagination(array('mid_size' => 2)); ?>
  </div>
</section>
<?php get_footer(); ?>

Here’s that template rendering real posts, and a single post with threaded comments:

Blog index - index.php + loops/blog-card.php

Single post - single.php + comments.php

Step 6: Page Templates for the Homepage and Specials

For pages that need bespoke layouts, WordPress page templates are the cleanest tool: a file with a Template Name header that editors assign from the page editor. Our homepage is front.tpl.php:

<?php
/**
 * Template Name: Front Page
 * Template Post Type: page
 */
get_header(); ?>

<section class="relative flex min-h-[80vh] items-center justify-center bg-ink"
         style="background-image:linear-gradient(rgba(16,24,40,.62),rgba(16,24,40,.62)),
                url('<?php echo esc_url(BT_ASSETS_DIRECTORY_URI . 'images/banner.jpg'); ?>');
                background-size:cover">
  <div class="container-site py-24 text-center">
    <h1 class="mx-auto max-w-3xl font-slab text-4xl text-white sm:text-5xl lg:text-6xl">
      <?php echo esc_html(get_bloginfo('name')); ?> -
      <strong class="font-bold"><?php echo esc_html(get_bloginfo('description')); ?></strong>
    </h1>
    <a href="<?php echo esc_url(home_url('/services/')); ?>" class="btn-primary mt-8">
      <?php esc_html_e('Explore Services', 'business-tailwind'); ?></a>
  </div>
</section>

Below the hero it renders the page’s own content (so editors control the intro copy), a services grid, animated counters, the latest posts via the same loops/blog-card.php partial, and a partner-logo carousel (container-bounded, arrow-controlled, with logos that reveal their true color on hover – the list is overridable via the bt_partner_logos filter). Assign it to a page, point Settings → Reading at that page, done. The same pattern gives you coming-soon.tpl.php and the 404 template.

Step 7: A Few Lines of Vanilla JavaScript

No jQuery needed. assets/js/business.js (~170 lines) handles the mobile nav toggle, counters and scroll-reveals with IntersectionObserver, the partner-logo carousel (gentle auto-scroll, seamless loop, prev/next arrows, pauses on hover) and a back-to-top button – enqueued in the footer with wp_enqueue_script(..., true). The whole file is in the download.

The theme stays sharp at every width, straight out of these templates:

Responsive - mobile view

Step 8: Ship It

Before zipping the theme, run the production build:

pnpm run build

Two workflow rules keep this painless in production:

  • Commit the compiled CSS. The theme installs and runs on any host with zero Node – the toolchain is only needed when you change the design.
  • Bump BT_THEME_VERSION whenever you rebuild, so caches refresh exactly once.

The final stylesheet: 27 KB minified, ~7 KB over the wire with gzip. That plus semantic markup – one h1 per view, landmark elements, breadcrumbs, loading="lazy" images – is the SEO foundation most themes bolt on afterwards, built in from the first commit.

Common Questions

Tailwind v3 or v4?
This guide uses Tailwind v3.4 and the classic tailwind.config.js workflow, which is what most WordPress toolchains and tutorials assume. Nothing about the theme structure changes with v4 – only the config format.

Do I need Node on my server?
No. The compiled CSS ships with the theme. Node/pnpm run only on your machine, at design time.

Should I use @apply everywhere so my templates look cleaner?
Resist it. @apply everything and you’ve reinvented a classic stylesheet with extra steps. We keep exactly one small components layer (buttons, cards, section titles, the WP menu bridge) – everything else is utilities in the template.

Does this work with Gutenberg?
It runs as a lean classic theme and dequeues block-editor CSS on the front end. Prefer blocks? Remove four wp_dequeue_style lines in functions.php and style .wp-block-* in your src/input.css.

How do plugins’ styles fit in?
Tailwind’s preflight resets only what your templates use; plugin output keeps its own CSS. For plugins you want restyled (forms, WooCommerce), target their classes inside @layer components.

Download the Finished Theme

The complete theme from this guide is free:

It seeds a starter menu on activation and includes the full Tailwind source (src/input.css, tailwind.config.js) so you can rebrand it by editing one config file.

Want It Built for You?

Custom Tailwind WordPress themes are our bread and butter – designed, built and tuned for Core Web Vitals. If you’d rather ship next week than learn a toolchain:

Book a free consultation with Bytes Brothers →

Interested in a Tailwind-powered WordPress theme?

We’ve built enough of them to know what works and what creates problems down the line. If you want a WordPress site that’s fast, easy to maintain, and doesn’t need a CSS archaeologist to update – a Tailwind theme is the right call.

→ See what a Tailwind WordPress build looks like