DEV Community

Cover image for How to add TailwindCSS to your Hugo site
Div Rhino
Div Rhino

Posted on • Updated on

How to add TailwindCSS to your Hugo site

Originally posted on

Tailwind is a utility CSS library that has been gaining a lot of popularity in the past few years. Utility libraries are great because you won't end up fighting against a framework when it comes to building your own custom designs. And with Tailwind, you can build your sites faster because you won't be jumping between your HTML and CSS all the time. In this tutorial, we're going to learn how to add TailwindCSS to a Hugo project.


To follow along with this tutorial, you will need to have Hugo and NodeJS installed

Installation guides:

Creating Hugo project and theme

Let's start by creating a new project. Navigation into the directory where you keep all your projects, and run the command to create a new hugo site. We're going to name our site "hugotails".

cd Sites
hugo new site hugotails
Enter fullscreen mode Exit fullscreen mode

Then change directories into our new "hugotails" site and create a new theme. We will call the theme "windytheme".

cd hugotails

hugo new theme windytheme
Enter fullscreen mode Exit fullscreen mode

Initialising NPM and installing dependencies

Now that we've got our hugo site all ready to go, let's install tailwind. To follow along with this section of the tutorial, you will need to have Nodejs installed. So if you haven't installed it already, you might want to do so before continuing.

Make sure you're in the "hugotails" project root because we will be initialising NPM here. We can run the npm init command with the --yes flag so we don't have to go through all the setup questions.

After this command is successfully run, we will have a newly-created package.json in our project root.

npm init --yes
Enter fullscreen mode Exit fullscreen mode

Now that our project has been initialised with NPM, we can go ahead and install the necessary packages. Run:

npm install --save-dev autoprefixer postcss postcss-cli postcss-import tailwindcss
Enter fullscreen mode Exit fullscreen mode

We are passing in the -save-dev flag because we want to save these packages as dev dependencies.

If you noticed, we aren't just installing the tailwind package, we have a few other accompanying packages. Below is a brief description of what each of them are for:

  1. Postcss - a tool for transforming CSS with JavaScript.
  2. postcss-cli - the command-line tool we can use to execute Postcss commands in the terminal
  3. postcss-import - used to resolve the path of an @import rule
  4. autoprefixer - helps us add vendor prefixes to our CSS
  5. tailwindcss - a library of utility classes to help us build unique custom layouts without doing too much

Setting up config files

Next up, we will tell our site to use our new windytheme theme, by adding the following line to our hugotails/config.toml file:

theme = "windytheme"
Enter fullscreen mode Exit fullscreen mode

Now we can move onto adding tailwind as a PostCSS plugin. Since we're working with a brand new theme, we don't have a css directory yet. Let's create one. Running mkdir with the -p flag will create nested directories if they don't already exist.

mkdir -p themes/windytheme/assets/css/
Enter fullscreen mode Exit fullscreen mode

Next we need to create configuration files for both PostCSS and tailwind. We will also need a main styles.scss file:

touch themes/windytheme/assets/css/postcss.config.js
touch themes/windytheme/assets/css/tailwind.config.js
touch themes/windytheme/assets/css/styles.scss
Enter fullscreen mode Exit fullscreen mode

Let's tackle the postcss config first. Open up the postcss.config.js file

const themeDir = __dirname + '/../../';

module.exports = {    
    plugins: [   
            path: [themeDir]
        require('tailwindcss')(themeDir + 'assets/css/tailwind.config.js'),
            path: [themeDir]
Enter fullscreen mode Exit fullscreen mode

Phew, that was a lot. But we're not done yet, let's head over to our tailwind.config.js and add a basic configuration file for TailwindCSS:

module.exports = {
  theme: {
    extend: {}
  variants: {},
  plugins: []
Enter fullscreen mode Exit fullscreen mode

Updating styles.scss

Now we have to include the necessary tailwind imports in our stylesheet. Open up our stylesheet, styles.scss, and add the following imports:

@import "node_modules/tailwindcss/base";
@import "node_modules/tailwindcss/components";
@import "node_modules/tailwindcss/utilities";
Enter fullscreen mode Exit fullscreen mode

Importing CSS in head tag

When we created our new theme, Hugo helped us out by creating some starting templates files. One of these starter files is the head.html partial.

{{ $styles := resources.Get "css/styles.scss" | toCSS | postCSS (dict "config" "./assets/css/postcss.config.js") }}
{{ if .Site.IsServer }}
  <link rel="stylesheet" href="{{ $styles.RelPermalink }}">
{{ else }}
  {{ $styles := $styles | minify | fingerprint | resources.PostProcess }}
  <link rel="stylesheet" href="{{ $styles.Permalink }}" integrity="{{ $styles.Data.Integrity }}">
{{ end }}
Enter fullscreen mode Exit fullscreen mode

So what's going on in our code above? Let's go over it line-by-line:

  • $styles := resources.Get "css/styles.scss" - We got our stylesheet as a resource and stored it in a variable called styles
  • | toCSS - We used Hugo pipes to first convert the SCSS file to CSS, so the browser can understand it
  • | postCSS (dict "config" "./assets/css/postcss.config.js") - We point to where our PostCSS config file lives
  • We're going to check if we're in our local dev environment by using the IsServer variable, if we are, we will link to the stylesheets relative URL
  • However, if we're in a production environment, we're going to use Hugo Pipes to minify our css file
  • We also want to pipe it through fingerprint so that we know the asset has not been tampered with. This also has a nice side-effect of cache-busting, so we also know we're serving the latest version

Testing some TailwindCSS classes

We've set up all the configuration we need. Now let's set up our index page too. This file can be found at themes/windytheme/layouts/index.html.

{{ define "main" }}
  {{ .Content }}
{{ end }}
Enter fullscreen mode Exit fullscreen mode

Now we can imported our styles and transformed them in a few useful ways, let's see if everything actually works.

Let's create a basic header in the themes/windytheme/layouts/partials/header.html file:

  <h1>Welcome to HugoTails!</h1>
Enter fullscreen mode Exit fullscreen mode

While still in themes/windytheme/layouts/partials/header.html, let's add some tailwind utility classes:

<header class="w-full bg-red-300">
  <h1 class="text-center">Welcome to HugoTails!</h1>
Enter fullscreen mode Exit fullscreen mode

We should also start our Hugo server:

hugo server
Enter fullscreen mode Exit fullscreen mode

Open your browser and visit http://localhost:1313, you should be able to see your changes there.


In this tutorial, you learnt how to add tailwindcss to your hugo site using Hugo Pipes. If you enjoyed this article and you'd like more, consider following Div Rhino on YouTube.

Congratulations, you did great. Keep learning and keep coding!

GitHub logo divrhino / hugotails

Add TailwindCSS to Your Hugo Site. Video tutorial available on the Div Rhino YouTube channel.

Top comments (9)

littleninja profile image
Sarah (she/her)

Thanks for this write-up! This is helping me work through Hugo pipes with Tailwind CSS ♥️

Small discrepancy with this note:

  • We also want to pipe it through fingerprint so that the browser does not cache old versions of our stylesheet

From what I've read, integrity is a security feature to make sure static resources haven't been tampered with, e.x. loading a JavaScript library from a cross-origin CDN. This may have cache-busting qualities, I'm not sure. Could you clarify?

littleninja profile image
Sarah (she/her)

Now that I've got Tailwind styles building and rendering (yay! and your article helped 💪🏻), time to purge! The un-optimized stylesheet is 5.95MB 😱. Previous setup made it under 3KB.

(Still working through it, would love to swap notes! Will be starting a new branch on my personal site repo:

divrhino profile image
Div Rhino

Cool, thanks for sharing the link to your branch.

Do you have any specific ideas in mind that you'd like swap notes on?

kareemsalah227 profile image
Kareem Salah

Thanks a lot Sarah for raising this, I wouldn't have seen this coming before reading your comment.

I would really appreciate if you have any tips on how to optimise and purge this CSS bundle.

This is my very first day with Hugo and I'm feeling so lost, can't come up with any ideas yet on how to optimise this particular CSS bundle.

Thread Thread
divrhino profile image
Div Rhino • Edited

Hello Kareem,

Although you've directed your comment above to Sarah, I thought I'd interject with some tips that may be helpful. Firstly, optimising CSS in this instance probably has more to do with TailwindCSS than with Hugo. So here are just a few things that have helped me:

  1. TailwindCSS allows you to use PurgeCSS to remove styles that aren't being used. Writing purgeable HTML helps the purge process. You can configure PurgeCSS in your tailwind.config.
  2. I often find that I tend to use a custom colour palette in most of the sites I design and build, so I avoid extending the Tailwind colours and just use custom colours in my config, instead
  3. Because flexbox and CSS grid are now pretty well-supported, I don't often find a need for certain things like floats. So being able to remove floats and similar features is very handy.

The tailwind docs have more useful optimisations.

Hope this is a useful starting point. :) 🍍

Thread Thread
littleninja profile image
Sarah (she/her)

This is a useful starting point! To add, the official Hugo docs show how to purge CSS with PostProcess. I was able to optimize my Tailwind styles without modifying the Tailwind config, but Div Rhino's suggestions can help you optimize further.

Here are the steps in code. These details are also in the link above to the Hugo docs:

First, in your config file:

  writeStats = true
Enter fullscreen mode Exit fullscreen mode

Next, in your PostCSS config file:

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: [ './hugo_stats.json' ],
    defaultExtractor: (content) => {
        let els = JSON.parse(content).htmlElements;
        return els.tags.concat(els.classes, els.ids);

module.exports = {
    plugins: [
        ...(process.env.HUGO_ENVIRONMENT === 'production' ? [ purgecss ] : [])
Enter fullscreen mode Exit fullscreen mode

Lastly, in your layout file (layouts/_default/baseOf.html or a style partial):

{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | resources.PostCSS }}
{{ if hugo.IsProduction }}
{{ $css = $css | minify | fingerprint | resources.PostProcess }}
{{ end }}
<link href="{{ $css.RelPermalink }}" rel="stylesheet" />
Enter fullscreen mode Exit fullscreen mode

To walk through what's happening here:

  1. Add configuration writeStats = true. If successful, this will write a hugo_stats.json file at the root of your theme or site.
  2. Add PurgeCSS with a custom extractor to your PostCSS config. The custom extractor will use the class names extracted by the hugo_stats.json and glob patterns to find your layout files to optimize Tailwind styles.
  3. Add a pipe to apply resources.PostProcess in the layout file where you define your styles.
divrhino profile image
Div Rhino

Hello Sarah,

Thank you for your comment. Yes, you are correct. The proper role of fingerprint is to ensure the resources have not been tampered with when they are being served.

However, in our case, we assume the assets are all being hosted on the same server, so a simple refresh could ensure we're getting the correct asset (i.e. no middle person intercepting it first).

Cache-busting is a nice side-effect, though. But I do see how the wording in the article makes it look like it is the main function. I will update the content to reflect your point. <3

cosmicspittle profile image
Robert Bell

Thank you for this write-up. It's very helpful.

FYI: I was getting a blank page after implementing the instructions. While reading through forums to figure it out, someone said Hugo had a problem with comments at the top of some files. I deleted the comment at the top of the index.html file and then it worked fine. I tried this on another system and the same thing happened - except I also needed to take out the top comment in header.html.

This is Hugo v0.80. I'm not sure what is going on here, but thought maybe others would like to know about it.

Thanks again!

divrhino profile image
Div Rhino • Edited

Hello Robert, thank you for adding this comment! <3 I'll look into this and either add version numbers or update the instructions to reflect your comments.

I probably missed it because I had added the comments at the top of code snippets of this article for clarity, but hadn't actually added any into the accompanying code repo. I'll move those into the article text, so others don't run into the same issue you did.