Eleventy: Integrate PostCSS and Tailwind CSS

About 2 min reading time

In my Eleventy tutorial I already showed how to set up a blog with Eleventy and also how to integrate CSS into an Eleventy website.

Since I like to work with Tailwind CSS, I naturally want to use it in my private projects as well and have tried a few setups here. Most of them use PostCSS and TailwindCSS "classically" and then adjust the scripts in the package.json so that the Eleventy page is built first and then the appropriate CSS is generated. You can do it that way, but in my eyes it made the setup in package.json more complicated.

My alternative uses Eleventy on-board resources to generate the CSS and fits into the existing configuration with just a few lines of code.

You can see the whole thing in my tutorial project in the tailwind branch (commit).

The idea behind it: Instead of a downstream process, we use PostCSS during the build via an asynchronous filter and thus generate our CSS directly via Eleventy as well.

In order to use PostCSS and Tailwind CSS, we need to install it in the first step:

npm i -D tailwindcss postcss autoprefixer cssnano

In our Eleventy configuration we now need an asynchronous filter:

const tailwind = require('tailwindcss');
const postCss = require('postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');

const postcssFilter = (cssCode, done) => {
	// we call PostCSS here.
	postCss([tailwind(require('./tailwind.config')), autoprefixer(), cssnano({ preset: 'default' })])
		.process(cssCode, {
			// path to our CSS file
			from: './src/_includes/styles/tailwind.css'
		})
		.then(
			(r) => done(null, r.css),
			(e) => done(e, null)
		);
};

Of course, you can also use only PostCSS with a few adjustments. Just leave the Tailwind CSS specific code out.

We now add the filter to the configuration, along with a watch target

module.exports = function (config) {
	// ...
	config.addWatchTarget('./src/_includes/styles/tailwind.css');
	config.addNunjucksAsyncFilter('postcss', postcssFilter);
	// ...
};

Via addWatchTarget we tell Eleventy that changes to this file should trigger a rebuild if we are currently running the application locally with --serve. The asynchronous filter ensures that we can convert our CSS.

Now we only need the CSS, which we add to ./src/_includes/styles/tailwind.css in the project. There we can of course work with Tailwind CSS as normal:

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

If we now want to add our CSS to the page, we have two options:

  1. as inline style anywhere in our template
  2. as own CSS file

Which variant is the right one here, depends strongly on the project and is probably worth a separate blogpost. I will quickly show both variants here:

In the head of the page (in our example in the file src/_includes/layout.njk):

<style>
	{% set css %}
	{% include "styles/tailwind.css" %}
	{% endset %}
	{{css | postcss | safe}}
</style>

Here we first set the css value in the template and then use the postcss filter. The safe filter is then used to insert the whole thing into our template. For me this is the right solution, if I have only little CSS and the generated HTML file is not too big.

If you prefer to create your own CSS file, you can do this with your own template (e.g. src/style.njk)

---
## permalink: style.css

{% set css %}
{% include "styles/tailwind.css" %}
{% endset %}
{{css | postcss | safe}}

Thanks to the permalink in the FrontMatter, this file is generated as style.css and can then be embedded in our template (src/_includes/layout.njk).

<link rel="stylesheet" href="/style.css" />

Do you know a better way to create and integrate CSS in Eleventy? Do you have more questions about Eleventy? Feel free to write me on Mastodon.


This post is based on my opinion and experience. It is based on what worked for me in my context. I recognize, that your context is different.
The "Just Sharing" Principle