Lessons

How To Integrate Tailwind CSS Into Your Theme and ACF Blocks

Integrating Tailwind into WordPress's block editor presents some challenges. Here are some solutions.


Naturally, the topic of this lesson is opinionated. If you’re going to build ACF blocks, you kinda need to have ACF Pro installed 🤷. You don’t have to use Tailwind though.

However, this course is all about showing folks the way I build lean, modern WordPress websites for publishers, SaaS, and enterprise.

So if that jives with you, cool. Read on!

If you feel this lesson isn't for you, feel free to jump to the non-Tailwind specific challenge here.

Steps in this lesson {class=“ignore-toc”}

  1. What Is Tailwind?
  2. Getting Tailwind Setup Locally
  3. Enqueueing Tailwind In Your Theme
  4. Tailwind In the Block Editor
  5. The Typography Plugin in the Editor
  6. Let’s Build a Tailwind Block

What is Tailwind?

Tailwind.css is a CSS framework. Tailwind’s goal is to help you:

Rapidly build modern websites without ever leaving your HTML.

As a utility-first CSS framework, you write your markup while composing classes like flex, pt-4, text-center and rotate-90.

The utility-first part means you’re reaching for utility classes first and writing traditional CSS only when a utility class won’t do. Having built more than one enterprise-sized WordPress website with Tailwind, I can say that I can go weeks without writing a single line of traditional CSS.

If I am writing traditional CSS, it’s likely because I’m styling something where I don’t have control over the markup. WordPress has a number of functions that produce their own markup. Most of the time you could add a filter to replace the markup while adding in Tailwind, but it may be faster to write the CSS. Just do what makes sense for you.

Getting Tailwind Setup Locally

The best guide on installing Tailwind is in their own docs, but here’s a brief list of the steps for getting Tailwind into your WordPress theme locally.

Install Tailwind CSS via npm

npm install -D tailwindcss
npx tailwindcss init

Configure your config

Define your paths

The npx command above should have created a tailwind.config.js file for you.

module.exports = {
    content: [
        "./blocks/**/*.{php,js}", 
        "./template-parts/**/*.php", 
        "./inc/**/*.php", 
        "./index.php",
        "./functions.php",
        "./src/**/*.js"
    ],
    theme: {
        extend: {},
    },
    plugins: [],
}

Tailwind CSS is incredibly performance focused and aims to produce the smallest CSS file possible by only generating the CSS you are actually using in your project. That means we need to let Tailwind know where to watch for Tailwind classes in our project.

Our paths go in the content array. Paths are configured as glob patterns, making it easy to match all of the content files in your project without a ton of configuration:

  • Use * to match anything except slashes and hidden files
  • Use ** to match zero or more directories
  • Use commas to separate values between {} to match against a list of options

Make sure to cover all of the paths that you’d expect to find Tailwind classes in - even your JavaScript.

Pattern gotchas

If you’re using Webpack, you will need to more strictly define your paths. Something like ./**/*.php will result in an infinite loop. If this affects you, you can read more here. The official recommendation is to either make your paths more specific like the initial example above or use the Tailwind CLI to handle your Tailwind stylesheet.

I recently gave the Fast Glob package a try in a client project using Webpack – it seemed to solve the infinite loop issue. It allowed us to simplify the “content” array of paths to our php templates:

module.exports = {
	important: true,
	content: require("fast-glob").sync(["./**/*.php"]),
	theme: {
		...
	}
}

Stylesheet

It’s up to you whether you want to create a new stylesheet for Tailwind, or roll it into your theme’s style.css. Either way, your stylesheet should contain these three Tailwind directives:

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

Start the build process

You can run Tailwind’s CLI to watch your project files and build the stylesheet (update the paths to match your project):

npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

Enqueueing Tailwind in your theme

From here on out, this lesson will be less about learning Tailwind and more about how to make the most of Tailwind in a WordPress theme.

You’re probably already familiar with enqueuing assets, but if you’re not:

/**
 * Enqueue scripts and styles.
 */
function my_theme_enqueue_scripts()
{
    // Enqueue style.css
	wp_enqueue_style('my_theme-style', get_stylesheet_uri());

    /** 
     * If your Tailwind stylesheet is separate, enqueue that too.
     * Update the path to match your project.
     **/
	wp_enqueue_style('tailwind-css', get_template_directory_uri() . '/dist/tailwind.css');
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );

Tailwind In the Block Editor

You should also enqueue Tailwind in the editor. I mean, that’s the point – to make your content in the editor look like what it looks like on the front-end.

function my_theme_editor_enqueue()
{
	wp_enqueue_style('tailwind-css', get_template_directory_uri() . '/dist/tailwind.css');
}
add_action('enqueue_block_editor_assets', 'my_theme_editor_enqueue');

Typography Plugin

Tailwind has a typography plugin that provides beautiful typographic defaults. Getting typography styles just right is difficult. Tailwind makes it easy. The typography stylesheet is applied to markup found within the .prose class:

<div class="prose">
    <h2>This heading would be styled</h2>
    <p>As would paragraphs.</p>
    <ul>
        <li>And lists</li>
        <li>And so on</li>
    </ul>
</div>

The most obvious place to use .prose is for blog posts – where you’d find long form content consisting mostly of typography blocks.

I’m not sure yet if I recommend applying .prose to the block editor on the backend. The reason one might want to do this is to create better parity between your front-end and backend.

In order to do this, you have’d to append the .prose class somewhere in the editor’s markup via JavaScript. That might look something like this:

(function () {
	let proseClassAdded = false;

	wp.data.subscribe(() => {
		addProseClass();
	});

	function addProseClass() {
		if (proseClassAdded) {
			return;
		}

		const editor = document.querySelector(".is-root-container");
		if (!editor) {
			return;
		}

		editor.classList.add("prose");
		proseClassAdded = true;
	}
})();

This would need to get enqueued for the block editor, just like our Tailwind stylesheet.

function my_theme_editor_enqueue()
{
	wp_enqueue_style( 'tailwind-css', get_template_directory_uri() . '/dist/tailwind.css' );
    wp_enqueue_script( 'editor-js', get_template_directory_uri() . '/dist/editor.js', ['wp-blocks', 'wp-element', 'wp-components', 'wp-i18n'] );
}
add_action('enqueue_block_editor_assets', 'my_theme_editor_enqueue');

The reason I’m hesitant to recommend this is that the typography stylesheet can affect parts of the block editor’s UI. I’ve kept a small list of CSS rules in an editor.css stylesheet that fixes small issues caused by the typography plugin, but it’s not ideal.

Just an idea

Another solution might be to create a “Prose Container” ACF block with inner blocks enabled. This would allow you to wrap ACF blocks and other core blocks inside of a .prose wrapper div – ensuring that only the content you want will be styled by the typography plugin.

Either way – you’re free to experiment and see what works best for you. Let me know how it goes!

Let’s Build a Block with Tailwind

We’re going to build a simple team member card block that takes the following fields:

  • Name
  • Avatar
  • Position
  • Bio
  • Social links

Team Member Field Group

We’ll need to scaffold our block:

/theme-root

└─── /blocks
│   │   register-blocks.php
│   │
│   └─── /team-member
│       │   block.json
│       │   block.php
│       │   template.php

In /blocks/register-blocks.php we’ll register the block by pointing to our block.json below:

{
    "name": "team-member",
    "title": "Team Member",
    "description": "Display a team member in a card",
    "category": "theme",
    "apiVersion": 2,
    "keywords": [
        "team",
        "member"
    ],
    "acf": {
        "mode": "preview",
        "renderTemplate": "blocks/team-member/block.php"
    },
    "supports": {
        "anchor": true
    }
}

In block.php I have:

$data = array(
	'name' => get_field( 'name' ),
	'avatar' => get_field( 'avatar' ),
	'position' => get_field('position'),
	'bio' => get_field( 'bio' ),
	'twitter_handle' => get_field( 'twitter_handle' ),
	'linkedin' => get_field( 'linkedin' ),
);


// Dynamic block ID
$block_id = 'team-member-' . $block['id'];

if( !empty($block['anchor']) ) {
	$block_id = $block['anchor'];
}

// Dynamic class names
$class_name = 'team-member';
if( !empty($block['className']) ) {
    $class_name .= ' ' . $block['className'];
}

get_template_part(
	'blocks/team-member/template',
	null,
	array(
		'block'      => $block,
		'is_preview' => $is_preview,
		'post_id'    => $post_id,
		'block_id'   => $block_id,
		'class_name'   => $class_name,
		'data'       => $data,
	)
);	

Before I start building my template with Tailwind classes, I’m going to make sure I have the Tailwind CLI running so that my Tailwind classes make it into the stylesheet:

npx tailwindcss -i ./src/input.css -o ./dist/output.css --watch

In template.php I have:

<?php
    $block = $args['block'];
    $data = $args['data'];
    $block_id = $args['block_id'];
    $class_name = $args['class_name'];
?>

<div id="<?php echo $block_id; ?>" class="<?php echo $class_name; ?> max-w-sm mt-12 shadow-lg not-prose">

	<!-- Avatar Image -->
	<img class="rounded-t-md w-full object-cover mx-auto" src="<?= esc_url($data['avatar']['url']); ?>" alt="<?= esc_attr($data['avatar']['alt']); ?>">

	<div class="rounded-b-md p-4">
		<!-- Name and Position -->
		<div class=" space-y-1 text-lg font-medium leading-6">
			<h3 class="text-lg font-semibold"><?= $data['name']; ?></h3>
			<p class="text-indigo-600"><?= $data['position']; ?></p>
		</div>

		<!-- Bio -->
		<div class="text-lg mt-4">
			<p class="text-gray-400"><?= $data['bio']; ?></p>
		</div>

		<!-- Social Links -->
		<ul role="list" class="flex space-x-5 mt-4 list-none p-0">
			<li>
				<a href="https://twitter.com/<?= esc_url($data['twitter_handle']); ?>" class="text-gray-400 hover:text-gray-500">
					<span class="sr-only">Twitter</span>
					<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84"></path></svg>
				</a>
			</li>
			<li>
				<a href="<?= esc_url($data['linkedin']); ?>" class="text-gray-400 hover:text-gray-500">
					<span class="sr-only">LinkedIn</span>
					<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M16.338 16.338H13.67V12.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.601 1.086-1.601 2.207v4.248H8.014v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.778 3.203 4.092v4.711zM5.005 6.575a1.548 1.548 0 11-.003-3.096 1.548 1.548 0 01.003 3.096zm-1.337 9.763H6.34v-8.59H3.667v8.59zM17.668 1H2.328C1.595 1 1 1.581 1 2.298v15.403C1 18.418 1.595 19 2.328 19h15.34c.734 0 1.332-.582 1.332-1.299V2.298C19 1.581 18.402 1 17.668 1z" clip-rule="evenodd"></path></svg>
				</a>
			</li>
		</ul>
	</div>
</div>

You could improve this template by wrapping optional fields in a if statement:

<?php 
    // Check if either Twitter or LinkedIn has been set
    if ( $data['twitter_handle'] || $data['linkedin'] ) :  ?>

        <!-- Social Links -->
        <ul role="list" class="flex space-x-5 mt-4 list-none p-0">
            <?php if ( $data['twitter_handle'] ) { ?>
                <li>
                    ... Twitter link and icon
                </li>
            <?php } ?>

            <?php if ( $data['linkedin'] ) { ?>
                <li>
                    ... LinkedIn link and icon
                </li>
            <?php } ?>
        </ul>

<?php endif ?>

This is what our block looks like in the backend:

Team Member block

Challenge!

Okay, it’s time for your first challenge. We’ll use all of the things we’ve learned so far to build something common in real world projects – pricing cards:

Pricing Cards Challenge

If you’re ready for the challenge, head over to the challenge page:

Let’s Go!


🤜 🤛 You did it!

If you've followed along, let me know how this went or if you have any questions.
@Joey_Farruggio or joey@joeyfarruggio.com