Eleventy: Integrate PostCSS and Tailwind CSS
In my Eleventy tutorial I showed how to set up a blog with Eleventy and how CSS gets into the pages. This goes one step further: Tailwind CSS in the same build.
The common approach runs the two side by side. The Tailwind CLI watches the stylesheet, Eleventy watches the templates, and package.json wires two scripts together with npm-run-all or concurrently. It works reliably, but it means two processes writing into the same output directory, and Eleventy knows nothing about the finished stylesheet.
Instead, I let Eleventy build the CSS itself. A hook generates the stylesheet before the first template renders and stores the resulting path as global data. One command, one process, and the layout knows the filename.
Dependencies
pnpm add -D tailwindcss @tailwindcss/postcss postcss cssnano@tailwindcss/postcss is Tailwind's PostCSS plugin; cssnano minifies the output for production. The Typography plugin used in the stylesheet below is optional and needs an extra pnpm add -D @tailwindcss/typography.
The stylesheet
Tailwind v4 is configured in CSS. There is no tailwind.config.js any more — plugins, source paths and design tokens all live in the entry file. Mine sits at src/_includes/styles/tailwind.css:
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@source '../../**/*.njk';
@source '../../**/*.md';
@theme {
--font-sans: 'Inter', sans-serif;
--color-accent: oklch(0.62 0.19 259);
}The @source lines tell the class scanner where the templates are. They are relative to the CSS file, so ../../ lands in src/. Tailwind can also discover sources on its own, but once the entry file sits deep inside src/_includes/, listing them explicitly is the more predictable option.
Everything under @theme becomes both a CSS variable and a utility, so --color-accent yields bg-accent, text-accent and so on.
The build hook
Eleventy fires the eleventy.before event once per build, before the first template renders. Exactly the right moment: the stylesheet is on disk before any page links to it.
eleventy.config.js:
import fs from 'fs';
import path from 'path';
import { createHash } from 'crypto';
import postcss from 'postcss';
import tailwindcss from '@tailwindcss/postcss';
import cssnano from 'cssnano';
const isProduction = process.env.ELEVENTY_PRODUCTION === 'true';
const entry = 'src/_includes/styles/tailwind.css';
const outputDir = '_site';
export default function (eleventyConfig) {
const assets = { css: '/theme.css' };
eleventyConfig.addWatchTarget('./src/_includes/styles/');
eleventyConfig.on('eleventy.before', async () => {
const source = fs.readFileSync(entry, 'utf8');
const { css } = await postcss([
tailwindcss(),
...(isProduction ? [cssnano({ preset: 'default' })] : [])
]).process(source, { from: entry });
const hash = createHash('sha256').update(css).digest('hex').slice(0, 8);
const filename = `theme-${hash}.css`;
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.join(outputDir, filename), css);
assets.css = `/${filename}`;
});
eleventyConfig.addGlobalData('assets', () => assets);
}The hook reads the entry file, runs it through PostCSS and writes the result into the output directory. cssnano is only in the plugin list for production; in dev mode that saves time and keeps the output readable. To run PostCSS without Tailwind, just drop tailwindcss().
The filename carries the first eight characters of a content hash. When the CSS changes, the URL changes, and the browser fetches the new file without any cache-header fiddling. The cost: every build with new content produces another file. As long as _site is cleared before the build that is a non-issue; otherwise stale copies pile up.
addGlobalData('assets', …) makes the object available to every template. The data cascade is assembled after eleventy.before, so the path is already up to date by then.
addWatchTarget makes Eleventy rebuild when you save in the styles directory while --serve is running. Without that line the dev server never notices stylesheet changes, because it is not a template.
Linking it
The layout never names the file directly, it reads the variable:
<link rel="stylesheet" href="/theme-5ddf772a.css" />Production
Minification hinges on an environment variable that the build script sets:
{
"scripts": {
"dev": "eleventy --serve",
"build": "ELEVENTY_PRODUCTION=true eleventy"
}
}If you also build on Windows, add cross-env.
This post was rewritten from scratch on 9 September 2026 and describes Eleventy 3 with Tailwind CSS v4.