Skip to content

Building a Production-Ready WordPress Theme with Sage and Tailwind CSS 4

Emnes
Design system illustration with color tokens around a central DESIGN SYSTEM label

A production WordPress theme in 2026 is very different from one built in 2020. Block editor support is non-negotiable. Performance budgets are strict — Core Web Vitals directly affect Google rankings. Accessibility is legally required in more jurisdictions every year. Design systems need to stay consistent between the frontend, the block editor, and any other surface rendering content. And all of this has to ship on a reasonable timeline without the theme becoming a maintenance liability.

Sage 11 combined with Tailwind CSS v4 is one of the strongest stacks for building production WordPress themes that meet all of these requirements. This guide is a practical walkthrough of building a real, production-ready theme from scratch: planning the design system, scaffolding Sage, configuring Tailwind v4 for the block editor, building Blade components, optimizing for Core Web Vitals, and shipping the whole thing to production.

This is a companion to our Sage 11 deep dive — that post covers the internals; this one focuses on the workflow of actually shipping a theme. If you haven’t used Sage before, read the deep dive first.

Before You Start: Plan the Design System

The biggest mistake new Sage developers make is jumping straight into Blade templates before defining the design system. The result is a theme where every template has slightly different button styles, spacing, and color usage — impossible to maintain, impossible to extend.

Spend 2–4 hours defining, in writing:

  • Color palette — primary, secondary, accent, neutrals, semantic (success/warning/error). Name each color in the palette as a Tailwind token.
  • Typography scale — 5–8 type sizes with specific line heights and letter-spacing. Pick one display font and one body font.
  • Spacing scale — Tailwind’s default 0.25rem progression is usually fine, but confirm and commit.
  • Breakpoints — mobile-first; sm, md, lg, xl, 2xl. Don’t invent new ones without a reason.
  • Component inventory — buttons, cards, form inputs, navigation items, hero sections, testimonials. List them.
  • Block editor tokens — which colors, font sizes, and spacing values should appear in the block editor’s design panel.

Document this as a single Markdown file in the theme’s repository. It becomes the single source of truth when design questions come up later.

Scaffold the Sage Theme

From your Bedrock site’s themes directory:

cd web/app/themes
composer create-project roots/sage your-theme
cd your-theme
pnpm install

Edit style.css with your theme metadata — name, URI, description, version, author. This header is what WordPress reads to display the theme in the admin, so take it seriously.

Activate the theme in WordPress. You should see a minimal page render with “Hello” — the Sage default.

Configure Tailwind v4 with a CSS-First Theme

Tailwind v4 moved configuration out of tailwind.config.js and into the CSS file itself. Open resources/css/app.css:

@import "tailwindcss";

@theme {
  /* Colors */
  --color-brand-primary: oklch(57% 0.2 260);
  --color-brand-primary-dark: oklch(47% 0.2 260);
  --color-brand-accent: oklch(74% 0.14 140);
  --color-neutral-50: oklch(98% 0.002 0);
  --color-neutral-900: oklch(18% 0.003 0);

  /* Typography */
  --font-family-display: "Inter", system-ui, sans-serif;
  --font-family-body: "Inter", system-ui, sans-serif;

  /* Type scale */
  --font-size-sm: 0.875rem;
  --font-size-base: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.25rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 1.875rem;
  --font-size-4xl: 2.25rem;
}

Every token becomes a Tailwind utility class: --color-brand-primary becomes bg-brand-primary, text-brand-primary, border-brand-primary. The OKLCH color space is new in Tailwind v4 and produces more perceptually-uniform gradients and hover states than hex or RGB.

Wire Tokens into the Block Editor

Sage’s Acorn package roots/acorn-theme-json reads your Tailwind configuration and generates theme.json automatically. In config/theme-json.php you can control what gets exposed to the block editor:

return [
  'settings' => [
    'color' => [
      'palette' => 'tailwind',
    ],
    'typography' => [
      'fontSizes' => 'tailwind',
      'fontFamilies' => 'tailwind',
    ],
    'spacing' => [
      'spacingSizes' => 'tailwind',
    ],
  ],
];

Build the theme with pnpm run build and check the generated theme.json at the theme root. Your brand colors and font sizes should appear in the block editor’s design panel.

Build the Layout Hierarchy

Sage’s Blade templates live in resources/views/. A practical hierarchy for most sites:

resources/views/
├── layouts/
│ └── app.blade.php # base layout with <html>, <head>, <body>
├── partials/
│ ├── header.blade.php
│ ├── footer.blade.php
│ └── main-menu.blade.php
├── sections/
│ ├── hero.blade.php
│ ├── testimonials.blade.php
│ └── cta.blade.php
├── components/
│ ├── button.blade.php
│ ├── card.blade.php
│ └── badge.blade.php
├── index.blade.php # default template
├── single.blade.php # single post
├── page.blade.php # single page
├── front-page.blade.php # home page
└── 404.blade.php

A minimal layouts/app.blade.php:

<!DOCTYPE html>
<html @php(language_attributes())>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  @php(wp_head())
</head>
<body @php(body_class('font-body antialiased'))>
  @include('partials.header')
  <main class="min-h-screen">@yield('content')</main>
  @include('partials.footer')
  @php(wp_footer())
</body>
</html>

Individual templates extend the layout and fill the content section:

@extends('layouts.app')

@section('content')
  <article class="prose prose-lg mx-auto py-12">
    <h1>{{ get_the_title() }}</h1>
    @php(the_content())
  </article>
@endsection

Build Reusable Components

Every element that appears more than twice should become a component. A components/button.blade.php:

@props(['href' => null, 'variant' => 'primary', 'size' => 'md'])

@php
  $base = 'inline-flex items-center font-semibold rounded-lg transition-colors';
  $variants = [
    'primary' => 'bg-brand-primary text-white hover:bg-brand-primary-dark',
    'secondary' => 'bg-neutral-100 text-neutral-900 hover:bg-neutral-200',
  ];
  $sizes = ['sm' => 'px-3 py-1.5 text-sm', 'md' => 'px-4 py-2', 'lg' => 'px-6 py-3 text-lg'];
  $classes = "$base {$variants[$variant]} {$sizes[$size]}";
@endphp

@if($href)
  <a href="{{ $href }}" class="{{ $classes }}">{{ $slot }}</a>
@else
  <button class="{{ $classes }}">{{ $slot }}</button>
@endif

Use anywhere:

<x-button href="/contact" variant="primary" size="lg">Get in Touch</x-button>

Custom Blocks for the Block Editor

For components that content editors need to place themselves, build custom blocks. Sage with Acorn has a scaffolding command:

wp acorn make:block Hero

This creates a block with server-side rendering (so the PHP renders the frontend, and the editor shows a preview) and editor UI. Customize the block’s Blade view to use your component library, and the design stays consistent between what the editor sees and what visitors see.

Performance: Core Web Vitals on Sage

Sage produces fast themes out of the box, but production performance is never automatic. Hit Core Web Vitals targets by:

  • Preload hero fonts — add <link rel="preload"> tags for the fonts used above the fold.
  • Inline critical CSS — Sage can do this via a build step; most small sites find the Tailwind utility-class approach produces small enough CSS that it’s not needed.
  • Lazy-load below-fold images — WordPress adds loading="lazy" by default; verify it’s present.
  • Use srcset for responsive images — the wp_get_attachment_image() helper handles this.
  • Minimize third-party scripts — analytics, chat widgets, marketing tags kill LCP. Load them defer/async, or move to server-side analytics.
  • Enable page caching — Trellis ships with FastCGI cache; other hosts have their own. Cached HTML bypasses PHP entirely.
  • Use Vite’s production bundlingpnpm run build produces minified, tree-shaken assets with content hashes for long-term caching.
  • Optimize images at upload time — enforce WebP or AVIF conversion for uploaded images.

Run Lighthouse on every PR. A target of 90+ on Performance, Accessibility, Best Practices, and SEO is achievable for Sage themes with reasonable care.

Accessibility Checklist

  • Semantic HTML — use <main>, <nav>, <article>, <header>, <footer> correctly. Don’t replace them with divs.
  • Heading hierarchy — one h1 per page, logical h2/h3 nesting. No skipping levels.
  • Skip-to-content link — the first focusable element should jump past the header.
  • Focus styles — never outline: none without a replacement. Tailwind’s focus:ring utilities are the easy path.
  • Color contrast — 4.5:1 for body text, 3:1 for large text. Verify every brand color combination.
  • Alt text enforcement — use a plugin or a review process that flags images uploaded without alt text.
  • Keyboard navigation — walk the site with only the keyboard. Every interactive element must be reachable and operable.
  • ARIA landmarksrole="navigation", aria-label on the header, etc., where semantic HTML isn’t enough.

Run axe or WAVE on every PR. Accessibility is cheapest to fix during development and most expensive to fix after launch.

Production Build and Deploy

Local dev uses pnpm run dev with Vite HMR. Production requires a full build:

pnpm install --frozen-lockfile
pnpm run build

The build output lands in public/build/ with content-hashed filenames. Two common deploy approaches:

Build in CI, Rsync to Production

Your CI pipeline runs pnpm install, pnpm run build, then rsyncs the built assets to the production server. public/build/ isn’t committed to git. This is the standard Trellis deploy-hook pattern.

Build on the Server

The production server has Node.js installed and runs the build as part of every deploy. Simpler for small setups; heavier resource requirements on the server.

At Emnes, our Trellis deploy hooks run the pnpm build in a dedicated deploy-hook script so the pattern is consistent across 18 sites.

Post-Launch Maintenance

  • Keep dependencies updated. composer update and pnpm update on a regular cadence, with a quick regression pass after each.
  • Run composer audit in CI with WP Sec Adv configured.
  • Monitor Core Web Vitals in Search Console. Regressions happen; catch them early.
  • Document component usage — a Storybook or a simple component-inventory Blade template pays off when the team grows.
  • Re-audit accessibility quarterly. Browser updates and new content can surface new issues.

Frequently Asked Questions

How long does a production Sage theme take to build?

For a medium-complexity site (10–15 page templates, 5–10 custom blocks, a well-defined design system): 2–4 weeks of full-time development for one senior developer. Simpler sites are faster; sites with lots of custom blocks or complex editorial workflows take longer.

Can I migrate a Sage 10 theme to Sage 11?

Yes. Blade templates port largely unchanged. The asset pipeline (Bud → Vite) needs work, and Tailwind v3 → v4 requires config changes. Plan a week for a medium-complexity theme migration.

Is Tailwind CSS too much utility-class noise in templates?

Tailwind templates look busy to some developers. Components solve this — once the utility soup is encapsulated in a component, usage sites are clean (<x-button variant="primary">). Combined with Blade components, Tailwind scales surprisingly well.

What if I need to support IE11 or very old browsers?

You can’t — Tailwind v4 uses modern CSS features (container queries, OKLCH colors, native nesting) that don’t exist in legacy browsers. For IE11 compatibility, stay on Tailwind v3. For all other evergreen browsers (Chrome, Firefox, Safari, Edge), v4 works fine.

How do I handle multi-language sites?

Standard WordPress i18n works with Sage. Wrap strings in __() / _e() in Blade: @{{ __('Read more', 'your-theme') }} or the Blade-native @lang('Read more'). For multi-site translation setups, plugins like WPML and Polylang are compatible.

Ship Great Themes, Stay Sane

Sage 11 plus Tailwind v4 is one of the most productive WordPress theme stacks available in 2026. The combination of Blade’s clean templating, Vite’s fast asset pipeline, Tailwind’s utility-first approach, and Acorn’s service container removes most of the pain points that make traditional WordPress theme development frustrating.

At Emnes, we’ve shipped dozens of production Sage themes across industries — SaaS products, media companies, ecommerce sites, agency portfolios. If you’re planning a custom theme build and want help with the design-system-to-Sage translation, get in touch.

Related reading: Sage 11 deep dive, Bedrock complete guide, the full Roots.io stack.