WordPress Design 16 min read

WordPress + Tailwind = Love

Published by Manuel Campos on December 17, 2025 • Updated on December 18, 2025

WordPress Tailwind

The other day I had a little bit of time off and decided it was about time to redesign one of my sites.

I asked Claude to recreate the homepage, and honestly, it did such a good job that I was genuinely impressed. The design was built with Tailwind CSS.

Up until then, I’d always asked Claude to recreate designs using vanilla CSS. It could do it, but the results never felt quite as polished as the Tailwind version.

So I decided to stick with Tailwind. Since then, I’ve already used it on three different sites and even in a custom PHP app.


Tailwind on Production

When you create a design using Tailwind CSS, you are gonna find this script on your source code:

 <script src="https://cdn.tailwindcss.com"></script>

That code includes a lot of extra stuffβ€”things you’ll probably never even use.

that script is also pretty heavy for most sites, and any WordPress speed expert would probably lose their mind if you just left it like that.


My Tailwind Plugin

I know there are probably plugins out there that can scan your themes and plugins for the Tailwind classes you’re actually using and then only enqueue the corresponding CSS.

However but I have eliminated my dependency on plugins.

When I need one, I rather create it using Artificial Intelligence.

This is the structure of the plugin that Claude provided for me.

wp-content/
└── mu-plugins/
    └── tailwind-handler/
        β”œβ”€β”€ tailwind.php            # Main plugin file
        β”œβ”€β”€ package.json            # npm dependencies
        β”œβ”€β”€ tailwind.config.js      # Tailwind configuration
        β”œβ”€β”€ postcss.config.js       # PostCSS configuration
        β”œβ”€β”€ src/
        β”‚   └── input.css           # Source Tailwind file
        β”œβ”€β”€ dist/
        β”‚   └── tailwind.min.css    # Compiled output (gitignored)
        └── node_modules/           # gitignored

package.json

{
  "name": "tailwind-handler",
  "version": "1.0.0",
  "description": "Tailwind CSS build system for WordPress",
  "scripts": {
    "build": "tailwindcss -i ./src/input.css -o ./dist/tailwind.min.css --minify",
    "watch": "tailwindcss -i ./src/input.css -o ./dist/tailwind.min.css --watch"
  },
  "devDependencies": {
    "tailwindcss": "^3.4.0"
  }
}

These are npm commands you can run from the terminal:

  • npm run build is a one-time command that generates a fully minified CSS file, which is ideal for production or deployment when you’re finished making changes.
  • npm run watch, on the other hand, keeps running in the background while you work, automatically rebuilding the CSS whenever you change Tailwind classes in your templates, so you don’t have to manually recompile during development.

You can change the location of where the file will be outputted, that ‘s really helpful for those of us who want to lock down wp-content and use a public folder instead.


tailwind.config.js

This file tells Tailwind where to look for class names and how to build your CSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    '../../themes/**/*.php',
    '../../themes/**/*.html',
    '../../themes/**/*.js',
  ],
  theme: {
    extend: {
      // Add your custom theme extensions here
    },
  },
  plugins: [],
}

If a file is missing from content, its classes will not appear in your final CSS.


postcss.config.js

This tells PostCSS which plugins to run when processing your CSS.

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

src/input.css

Tailwind covers most layout and styling needs, but there are still cases where custom CSS (the part ) is useful or necessary.

You can add all of that under custom CSS and that will be included on Tailwind file generated by the plugin.

@tailwind base;
@tailwind components;
@tailwind utilities;
/* Add any custom CSS here */

NPM Install

Go to the folder where your Tailwind mu-plugin is

Run this command

   npm install

You will get this message on your terminal

up to date, audited 74 packages in 705ms
18 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities

Result

Now, all I had to do was to go to the folder via the terminal and run this command:

npm run build

then a CSS file would be generated when the process was over and all I had to do was to link to that file from the header

The problem was that the file is created within the mu-plugin and that’s not a folder that I intend for the web to access

so I had to move it to the assets’ folder manually

<link rel="stylesheet" href="<?php echo site_url('/assets/files/tailwind.min.css'); ?>" type="text/css" media="all" />

Not much work but I don’t want to do that manually every time I changed something on the theme.


My Buddy Claude

My Buddy Claude told me that the plugin could handle the process but I needed to install Node Package Manager.

I am working locally using Podman so I have to do this for the container where that site was built on.

Step #1 – Access the Container


podman exec -it tlb_wordpress_site bas

Step #2 – Check the OS


cat /etc/os-release

Step #3 – Install NodeJS and NPM


apt-get update
apt-get install -y nodejs npm

Step #4 – Verify installation

which npm
npm --version

Step #5 – Exit Container

exit

Main

This is the main file that ensure I can rebuild the file from my WordPress dashboard so I don’t have to type a line of code.

It works

Maybe there is something on it which it shouldn’t be there.

<?php
/**
 * Plugin Name: Tailwind CSS Handler for WordPress (Local + Podman)
 * Description: Compiles and manages Tailwind CSS for WordPress Themes
 * Version: 2.0.0
 */
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class TailwindHandler {
    private $plugin_dir;
    private $plugin_url;
    private $dist_file;
    private $copy_destination_dir;
    private $copy_destination_file;
    private $copy_destination_url;
    private $npm_path_override = null; // Set in constructor if needed
    public function __construct() {
        // Initialize paths
        $this->plugin_dir = dirname(__FILE__);
        $this->plugin_url = plugins_url('', __FILE__);
        $this->dist_file = $this->plugin_dir . '/dist/tailwind.min.css';
        // MANUAL NPM PATH OVERRIDE - For Podman/Docker containers
        // Set to null to use auto-detection, or specify the path
        $this->npm_path_override = '/usr/bin/npm'; // Standard path in Debian-based containers
        // Define the target copy path as absolute path in WordPress root
        $this->copy_destination_dir = ABSPATH . 'assets/files';
        $this->copy_destination_file = $this->copy_destination_dir . '/tailwind.min.css';
        $this->copy_destination_url = home_url('/assets/files/tailwind.min.css');
        // Hook into WordPress
        add_action('wp_enqueue_scripts', [$this, 'enqueue_tailwind']);
        add_action('admin_notices', [$this, 'check_compiled_css']);
        add_action('admin_post_rebuild_tailwind', [$this, 'rebuild_tailwind']);
        add_action('admin_menu', [$this, 'add_admin_page']);
        // WP-CLI command
        if (defined('WP_CLI') && WP_CLI) {
            WP_CLI::add_command('tailwind rebuild', [$this, 'cli_rebuild']);
        }
    }
    /**
     * Enqueue the compiled Tailwind CSS from the copied location
     */
    public function enqueue_tailwind() {
        if (file_exists($this->copy_destination_file)) {
            wp_enqueue_style(
                'tailwind-css',
                $this->copy_destination_url,
                [],
                filemtime($this->copy_destination_file)
            );
        }
    }
    /**
     * Check if compiled CSS exists and show notice if missing
     */
    public function check_compiled_css() {
        if (!file_exists($this->copy_destination_file) && current_user_can('manage_options')) {
            $rebuild_url = wp_nonce_url(
                admin_url('admin-post.php?action=rebuild_tailwind'),
                'rebuild_tailwind_nonce'
            );
            ?>
            <div class="notice notice-warning is-dismissible">
                <p>
                    <strong>Tailwind CSS:</strong> Compiled CSS file not found.
                    <a href="<?php echo esc_url($rebuild_url); ?>" class="button button-primary">
                        Build Tailwind CSS now
                    </a>
                </p>
            </div>
            <?php
        }
    }
    /**
     * Add admin page under Tools menu
     */
    public function add_admin_page() {
        add_management_page(
            'Tailwind CSS Handler',
            'Tailwind CSS',
            'manage_options',
            'tailwind',
            [$this, 'admin_page']
        );
    }
    /**
     * Render admin page
     */
    public function admin_page() {
        if (!current_user_can('manage_options')) {
            wp_die(__('You do not have sufficient permissions to access this page.'));
        }
        // Check various statuses
        $compiled_exists = file_exists($this->dist_file);
        $compiled_time = $compiled_exists ? date('Y-m-d H:i:s', filemtime($this->dist_file)) : 'Never';
        $node_modules_exists = is_dir($this->plugin_dir . '/node_modules');
        $copied_exists = file_exists($this->copy_destination_file);
        $copied_time = $copied_exists ? date('Y-m-d H:i:s', filemtime($this->copy_destination_file)) : 'Never';
        // Check directory permissions
        $dest_dir_exists = is_dir($this->copy_destination_dir);
        $dest_dir_writable = $dest_dir_exists ? is_writable($this->copy_destination_dir) : is_writable(dirname($this->copy_destination_dir));
        // DEBUG: Check npm detection
        $detected_npm = $this->find_npm();
        $npm_exists = file_exists($detected_npm);
        $npm_executable = $npm_exists ? is_executable($detected_npm) : false;
        // Generate nonce URL
        $rebuild_url = wp_nonce_url(
            admin_url('admin-post.php?action=rebuild_tailwind'),
            'rebuild_tailwind_nonce'
        );
        ?>
        <div class="wrap">
            <h1>Tailwind CSS Handler</h1>
            <?php settings_errors('tailwind_messages'); ?>
            <div class="card">
                <h2>Status</h2>
                <table class="widefat">
                    <tbody>
                        <tr>
                            <td><strong>Node Modules:</strong></td>
                            <td>
                                <?php if ($node_modules_exists): ?>
                                    <span style="color: green;">βœ“ Installed</span>
                                <?php else: ?>
                                    <span style="color: red;">βœ— Not installed</span>
                                    <p><em>Run <code>npm install</code> in the plugin directory</em></p>
                                <?php endif; ?>
                            </td>
                        </tr>
                        <tr style="background: #f9f9f9;">
                            <td><strong>NPM Detection (DEBUG):</strong></td>
                            <td>
                                Detected path: <code><?php echo esc_html($detected_npm); ?></code><br>
                                File exists: <?php echo $npm_exists ? '<span style="color: green;">βœ“ Yes</span>' : '<span style="color: red;">βœ— No</span>'; ?><br>
                                Is executable: <?php echo $npm_executable ? '<span style="color: green;">βœ“ Yes</span>' : '<span style="color: red;">βœ— No</span>'; ?>
                            </td>
                        </tr>
                        <tr>
                            <td><strong>Compiled CSS (Build):</strong></td>
                            <td>
                                <?php if ($compiled_exists): ?>
                                    <span style="color: green;">βœ“ Exists</span><br>
                                    Last compiled: <?php echo esc_html($compiled_time); ?><br>
                                    <small>Path: <code><?php echo esc_html($this->dist_file); ?></code></small>
                                <?php else: ?>
                                    <span style="color: red;">βœ— Not compiled</span>
                                <?php endif; ?>
                            </td>
                        </tr>
                        <tr>
                            <td><strong>Destination Directory:</strong></td>
                            <td>
                                <?php if ($dest_dir_exists): ?>
                                    <span style="color: green;">βœ“ Exists</span>
                                    <?php if ($dest_dir_writable): ?>
                                        <span style="color: green;">βœ“ Writable</span>
                                    <?php else: ?>
                                        <span style="color: red;">βœ— Not writable</span>
                                    <?php endif; ?>
                                <?php else: ?>
                                    <span style="color: orange;">⚠ Will be created</span>
                                <?php endif; ?>
                                <br>
                                <small>Path: <code><?php echo esc_html($this->copy_destination_dir); ?></code></small>
                            </td>
                        </tr>
                        <tr>
                            <td><strong>Public CSS File:</strong></td>
                            <td>
                                <?php if ($copied_exists): ?>
                                    <span style="color: green;">βœ“ Exists</span><br>
                                    Last updated: <?php echo esc_html($copied_time); ?><br>
                                    <small>Path: <code><?php echo esc_html($this->copy_destination_file); ?></code></small><br>
                                    <small>URL: <code><?php echo esc_html($this->copy_destination_url); ?></code></small>
                                <?php else: ?>
                                    <span style="color: red;">βœ— Not copied</span><br>
                                    <small>Will be created on build</small>
                                <?php endif; ?>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <div class="card">
                <h2>Build Tailwind CSS</h2>
                <p>This will scan your theme files and generate optimized Tailwind CSS.</p>
                <p>The CSS will be copied to <code><?php echo esc_html($this->copy_destination_file); ?></code></p>
                <?php if (!$node_modules_exists): ?>
                    <p style="color: red;"><strong>Warning:</strong> Please run <code>npm install</code> in the plugin directory first.</p>
                <?php endif; ?>
                <p>
                    <a href="<?php echo esc_url($rebuild_url); ?>" class="button button-primary button-large">
                        ➀ Rebuild Tailwind CSS
                    </a>
                </p>
            </div>
            <div class="card">
                <h2>Setup Instructions</h2>
                <ol>
                    <li>Navigate to the plugin directory: <code><?php echo esc_html($this->plugin_dir); ?></code></li>
                    <li>Run: <code>npm install</code></li>
                    <li>Click "Rebuild Tailwind CSS" button above</li>
                    <li>Your theme can now use Tailwind utility classes</li>
                </ol>
                <h3>WP-CLI Command</h3>
                <p>You can also rebuild via command line:</p>
                <p><code>wp tailwind rebuild</code></p>
            </div>
        </div>
        <?php
    }
    /**
     * Handle rebuild request from admin
     */
    public function rebuild_tailwind() {
        check_admin_referer('rebuild_tailwind_nonce');
        if (!current_user_can('manage_options')) {
            wp_die(__('Unauthorized'));
        }
        $result = $this->run_build();
        if ($result['success']) {
            add_settings_error(
                'tailwind_messages',
                'tailwind_rebuilt',
                'Tailwind CSS rebuilt and copied successfully!',
                'success'
            );
        } else {
            $error_message = 'Error: ' . $result['message'];
            if (isset($result['output']) && !empty($result['output'])) {
                $error_message .= '<br><pre style="background: #f0f0f0; padding: 10px; overflow-x: auto;">' . 
                    esc_html($result['output']) . '</pre>';
            }
            add_settings_error(
                'tailwind_messages',
                'tailwind_error',
                $error_message,
                'error'
            );
        }
        set_transient('settings_errors', get_settings_errors(), 30);
        wp_redirect(add_query_arg('settings-updated', 'true', admin_url('tools.php?page=tailwind')));
        exit;
    }
    /**
     * WP-CLI rebuild command
     */
    public function cli_rebuild($args, $assoc_args) {
        WP_CLI::log('Building Tailwind CSS...');
        $result = $this->run_build();
        if ($result['success']) {
            WP_CLI::success('Tailwind CSS built and copied successfully!');
            if (!empty($result['output'])) {
                WP_CLI::log('Output: ' . $result['output']);
            }
        } else {
            WP_CLI::error('Build failed: ' . $result['message']);
        }
    }
    /**
     * Find npm executable path
     */
    private function find_npm() {
        // Check for manual override first
        if ($this->npm_path_override && file_exists($this->npm_path_override) && is_executable($this->npm_path_override)) {
            return $this->npm_path_override;
        }
        // Try common npm paths in order
        $npm_paths = [
            '/usr/local/bin/npm',
            '/usr/bin/npm',
            '/opt/homebrew/bin/npm',
            '/home/' . get_current_user() . '/.nvm/versions/node/v18.0.0/bin/npm', // NVM path
            '/root/.nvm/versions/node/v18.0.0/bin/npm', // Root NVM
        ];
        foreach ($npm_paths as $path) {
            if (file_exists($path) && is_executable($path)) {
                return $path;
            }
        }
        // Try to find npm via which command with expanded PATH
        $which_result = @shell_exec('PATH=/usr/local/bin:/usr/bin:/bin:~/.nvm/versions/node/v18.0.0/bin which npm 2>/dev/null');
        if (!empty($which_result)) {
            $path = trim($which_result);
            if (file_exists($path) && is_executable($path)) {
                return $path;
            }
        }
        // Last resort fallback
        return 'npm';
    }
    /**
     * Run the actual build process
     */
    private function run_build() {
        // Check if node_modules exists
        if (!is_dir($this->plugin_dir . '/node_modules')) {
            return [
                'success' => false,
                'message' => 'node_modules not found. Please run npm install first.'
            ];
        }
        // Ensure dist directory exists
        $dist_dir = $this->plugin_dir . '/dist';
        if (!is_dir($dist_dir)) {
            if (!wp_mkdir_p($dist_dir)) {
                return [
                    'success' => false,
                    'message' => 'Could not create dist directory: ' . $dist_dir
                ];
            }
        }
        // Find npm executable
        $npm = $this->find_npm();
        // Run the build command with full npm path (no need for PATH if we have absolute path)
        $command = sprintf(
            'cd %s && %s run build 2>&1',
            escapeshellarg($this->plugin_dir),
            escapeshellarg($npm)
        );
        exec($command, $output, $return_var);
        $output_string = implode("\n", $output);
        // Check if build was successful
        if ($return_var !== 0) {
            return [
                'success' => false,
                'message' => 'Build command failed with exit code ' . $return_var . '. npm path: ' . $npm,
                'output' => $output_string
            ];
        }
        // Verify the dist file was created
        if (!file_exists($this->dist_file)) {
            return [
                'success' => false,
                'message' => 'Build completed but dist file not found at: ' . $this->dist_file,
                'output' => $output_string
            ];
        }
        // Copy the file
        $copy_result = $this->copy_dist_file();
        if (!$copy_result['success']) {
            $copy_result['output'] = $output_string;
            return $copy_result;
        }
        return [
            'success' => true,
            'message' => 'Build and copy completed successfully',
            'output' => $output_string
        ];
    }
    /**
     * Copies the compiled CSS to the assets directory
     */
    private function copy_dist_file() {
        // Verify source file exists
        if (!file_exists($this->dist_file)) {
            return [
                'success' => false,
                'message' => 'Source file does not exist: ' . $this->dist_file
            ];
        }
        // Ensure the destination directory exists using WordPress function
        if (!wp_mkdir_p($this->copy_destination_dir)) {
            return [
                'success' => false,
                'message' => 'Could not create target directory: ' . $this->copy_destination_dir . ' (Check permissions)'
            ];
        }
        // Verify directory is writable
        if (!is_writable($this->copy_destination_dir)) {
            return [
                'success' => false,
                'message' => 'Target directory is not writable: ' . $this->copy_destination_dir . ' (chmod 755 or 775)'
            ];
        }
        // Use WordPress filesystem API for better compatibility
        require_once(ABSPATH . 'wp-admin/includes/file.php');
        // Perform the copy operation
        if (!copy($this->dist_file, $this->copy_destination_file)) {
            $error = error_get_last();
            return [
                'success' => false,
                'message' => 'Failed to copy file. ' . ($error ? $error['message'] : 'Unknown error')
            ];
        }
        // Verify the copy was successful
        if (!file_exists($this->copy_destination_file)) {
            return [
                'success' => false,
                'message' => 'Copy operation completed but file not found at destination'
            ];
        }
        return [
            'success' => true,
            'message' => 'File copied successfully to ' . $this->copy_destination_file
        ];
    }
}
// Initialize the plugin
new TailwindHandler();

Disclaimer

I use Podman to create local instances of WordPress,so I don’t really pay much attention to the security side of things of this plugin and other plugin I have built.

Having said that, Despite my sites being fully static and there is no database, installation or server on the internet to hack.

I have built custom firewall rules to prevent bots from scanning theme, plugins and mu-plugins folders.


Manuel Campos

Manuel Campos

I'm a WordPress enthusiast. I document my journey and provide actionable insights to help you navigate the ever-evolving world of WordPress."

Read Next

Support Honest Reviews

Help keep the reviews coming by using my recommended links.

May earn commission β€’ No extra cost to you