26.4 Setting Up Tailwind CSS with Hugo
Right, let’s get our hands dirty. You’re about to make Hugo and Tailwind play nice together, which is a fantastic idea. You get the component-driven, content-focused power of Hugo and the rapid, utility-first styling of Tailwind. But their default setups are like two brilliant people who speak different languages; we need to build a solid interpreter between them. We’re going to process your Tailwind CSS on the fly, as part of Hugo’s build process. This is the way.
The Core Concept: Hugo Pipes and PostCSS
Hugo isn’t a Node.js project. You don’t run npm run build to generate your site. Hugo is a standalone binary that does its own thing. So, to get Tailwind—which is a PostCSS plugin—into the mix, we use Hugo’s built-in asset pipeline, called Hugo Pipes. It can process your CSS files with PostCSS, and that’s our golden ticket.
We’ll tell Hugo: “Hey, see this CSS file? When you process it, run PostCSS on it. And here’s a list of PostCSS plugins to use, one of which is Tailwind.” Hugo handles the rest, purging your unused CSS based on your content and templates. It’s shockingly elegant once it’s configured.
The Step-by-Step Setup
First, you need to initialize a Node.js project in the root of your Hugo site. This isn’t for building the site, but solely to manage the PostCSS and Tailwind dependencies that Hugo Pipes will use.
npm init -y
Next, let’s install the required packages. We need Tailwind itself, and PostCSS with the CLI—because Hugo, somewhat oddly, uses the PostCSS CLI under the hood.
npm install -D tailwindcss postcss postcss-cli
Now, generate your tailwind.config.js file. This is non-negotiable.
npx tailwindcss init
This creates the config file. Open it. We need to tell Tailwind where to look for classes to purge. This is the most common “why isn’t my style showing up?!” pitfall. Your content sources should include all your HTML templates, Markdown files, and any other files that might generate HTML with classes.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./layouts/**/*.html",
"./content/**/*.md",
"./assets/**/*.css", // Only if you put classes in here!
],
theme: {
extend: {},
},
plugins: [],
}
Be aggressive here. If you create a new partial in layouts/partials, it needs to be covered by that ./layouts/**/*.html glob. If you put it somewhere else, add that path.
The Magic File: assets/css/tailwind.css
This is your source CSS file. Create it at assets/css/tailwind.css. This file does one job: it imports the Tailwind directives. Don’t put your actual custom CSS here; think of this as the entry point for the Tailwind engine.
@tailwind base;
@tailwind components;
@tailwind utilities;
The Linchpin: hugo.yaml (or hugo.toml)
This is where we wire it all together. In your site’s configuration file, you tell Hugo to take that source CSS file, process it with PostCSS (using the plugins we defined via Node), and then output the final, optimized CSS.
If you’re using YAML (my preference), it looks like this:
module:
imports:
- path: github.com/gohugoio/hugo-mod-jslib-distributor/presets/modern
build:
writeStats: true # Optional, but useful for debugging
params:
# ... your other params
assets:
disableHLJS: true # Optional: disable Hugo's built-in highlighting if you use Tailwind's
The critical part is defining the pipeline in your templates. You’ll do this in your base template (like layouts/partials/head.html). This is the command that makes it all happen:
{{- with resources.Get "css/tailwind.css" | postCSS (dict "config" "./postcss.config.js") -}}
{{- if eq hugo.Environment "development" -}}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else -}}
{{- with . | minify | fingerprint -}}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end -}}
{{- end -}}
{{- end -}}
Let’s break down that mouthful. We get the resource from our assets, pipe it to the postCSS function, and tell it where to find its config. In development, we just output the CSS link. In production, we also minify and fingerprint it for performance and cache-busting.
The PostCSS Config
You must create a postcss.config.js file in the root of your project. This is a simple bridge file that tells the PostCSS CLI what plugins to use.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
And there you have it. Run hugo server and watch the magic happen. Hugo will generate your CSS, and it will automatically purge any unused Tailwind classes, resulting in a tiny, optimized stylesheet. The first time you see it work, you’ll feel like you’ve just performed a minor act of wizardry. Because you have. Now go build something beautiful without writing a single line of margin-left: 12px;.