Add Tailwind v4 to a WordPress Block Theme Without a Plugin

To use Tailwind with a WordPress block theme without a plugin, you have to solve three problems. You need Tailwind v4 classes that use the colors, fonts and spacing already defined in theme.json, not a second palette. Those classes have to work on the front end and inside the editor iframe. And Tailwind has to find the classes in your block markup, which lives in .html templates, parts and patterns rather than in JSX.

This snippet covers all three. A WP-CLI command turns your theme.json presets into a Tailwind v4 @theme file, which means bg-primary points at var(--wp--preset--color--primary). Change a color in the Site Editor and every Tailwind class follows. One enqueue_block_assets callback loads the compiled CSS on the front end and in the editor canvas. The build itself is the official Tailwind CLI, so you don’t need a bundler.

The snippet

Put this in your block theme’s functions.php or in a small plugin. It expects the compiled file at assets/css/tailwind.css in the theme.

<?php
/**
 * Tailwind v4 in a block theme, no plugin.
 */

function dpc_tailwind_css_path(): string {
	return get_theme_file_path( 'assets/css/tailwind.css' );
}

/**
 * Front end and the editor iframe both fire enqueue_block_assets.
 */
function dpc_enqueue_tailwind(): void {
	$path = dpc_tailwind_css_path();
	if ( ! file_exists( $path ) ) {
		return;
	}
	wp_enqueue_style(
		'dpc-tailwind',
		get_theme_file_uri( 'assets/css/tailwind.css' ),
		array(),
		(string) filemtime( $path )
	);
}
add_action( 'enqueue_block_assets', 'dpc_enqueue_tailwind' );

/**
 * Build a Tailwind v4 @theme block that points at theme.json presets.
 */
function dpc_tailwind_theme_tokens(): string {
	$settings = wp_get_global_settings();

	// Tailwind namespace => [ theme.json section, key, WP preset name, class prefix ].
	$groups = array(
		'color'   => array( 'color', 'palette', 'color', '' ),
		'font'    => array( 'typography', 'fontFamilies', 'font-family', '' ),
		'text'    => array( 'typography', 'fontSizes', 'font-size', '' ),
		'spacing' => array( 'spacing', 'spacingSizes', 'spacing', 'wp-' ),
	);

	$tokens = array();
	foreach ( $groups as $namespace => list( $section, $key, $preset, $prefix ) ) {
		$presets = $settings[ $section ][ $key ] ?? array();
		foreach ( array( 'theme', 'custom' ) as $origin ) {
			foreach ( $presets[ $origin ] ?? array() as $item ) {
				if ( empty( $item['slug'] ) || ! is_string( $item['slug'] ) ) {
					continue;
				}
				$slug = preg_replace( '/[^a-z0-9-]/', '', strtolower( _wp_to_kebab_case( $item['slug'] ) ) );
				if ( '' === $slug ) {
					continue;
				}
				$tokens[ "--{$namespace}-{$prefix}{$slug}" ] = "var(--wp--preset--{$preset}--{$slug})";
			}
		}
	}

	$css  = "/* Generated from theme.json by `wp dpc tailwind-tokens`. Do not edit. */\n";
	$css .= "@theme inline {\n";
	foreach ( $tokens as $name => $value ) {
		$css .= "\t{$name}: {$value};\n";
	}
	$css .= "}\n";

	return $css;
}

if ( defined( 'WP_CLI' ) && WP_CLI ) {
	WP_CLI::add_command(
		'dpc tailwind-tokens',
		static function (): void {
			WP_CLI::line( dpc_tailwind_theme_tokens() );
		}
	);
}

Next, create src/tailwind.css in the theme. This is the Tailwind entry file:

/* Theme variables + utilities. No preflight: theme.json owns base styles. */
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css";
@import "./wp-tokens.css";

/* Block markup lives here, not in JS. Paths are relative to this file. */
@source "../templates";
@source "../parts";
@source "../patterns";
@source "../functions.php";

/* Classes typed into the editor are stored in the database. List them here. */
@source inline("{hover:,}bg-primary text-contrast");

Build it from the theme folder:

npm install -D tailwindcss @tailwindcss/cli

# Regenerate tokens whenever theme.json presets change
wp dpc tailwind-tokens > src/wp-tokens.css

# Production build
npx @tailwindcss/cli -i src/tailwind.css -o assets/css/tailwind.css --minify

# While developing
npx @tailwindcss/cli -i src/tailwind.css -o assets/css/tailwind.css --watch

How it works

  • dpc_tailwind_theme_tokens() reads the merged settings with wp_get_global_settings(), takes the theme and user (custom) presets, and writes one Tailwind variable per preset. The color namespace becomes bg-*, text-* and border-*, font becomes font-*, and text becomes the font-size utilities.
  • @theme inline makes Tailwind put the var(--wp--preset--…) reference straight into each utility. WordPress already prints those variables in global styles, so a palette change in the Site Editor updates your Tailwind classes without a rebuild.
  • Spacing tokens get a wp- prefix (p-wp-50, gap-wp-30). That keeps them apart from Tailwind’s own numeric scale, so p-4 still means what Tailwind says it means.
  • enqueue_block_assets runs on the front end and inside the iframed editor canvas, so blocks look the same in both places. The filemtime() version busts caches after every build.
  • The @source lines tell Tailwind to scan your templates, parts and patterns, and only classes found there end up in the CSS. That is the v4 replacement for the old content array and purge config.

Customise it

  • Want only your theme palette and none of Tailwind’s default colors? Add --color-*: initial; as the first line inside a @theme block in src/tailwind.css.
  • Need Tailwind’s preflight reset for a theme with no theme.json styles? Swap the first two imports for @import "tailwindcss";. Expect it to fight core block styles.
  • To pick up the default origin as well (core’s color palette and spacing scale), add 'default' to the origins array.

Gotchas

  • Tailwind only sees files on disk. A class someone types into the Additional CSS class(es) field of a block in a post is stored in the database, so it gets purged unless you list it in @source inline(). For patterns and templates, keep the classes in the theme files.
  • Utilities are imported without a cascade layer on purpose. WordPress global styles are not layered, and unlayered CSS always beats layered CSS, so layered utilities would silently lose. This is the usual cause of “tailwind v4 theme not working” in block themes.
  • Run wp dpc tailwind-tokens again after adding or renaming a preset. Changing a preset’s value doesn’t need a rebuild, but adding or renaming one does.
  • Keep node_modules out of the theme zip you ship. Only assets/css/tailwind.css is needed at runtime.

If you only want custom editor CSS without Tailwind, see CSS in the Gutenberg editor. To control which core block CSS still loads, see Remove Gutenberg Block Library CSS.

If you’d rather not run a build step, Winden compiles Tailwind on the server and works with Gutenberg.

Snippet poll. Should this snippet be in the Scripts Organizer library for one-click import? 10 votes put it there.

1 of 10 votes

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.