Using Tailwind CSS with Hugo

Reading time: 4 min

In this article we are going to see how can we build our Hugo site with TailwindCSS. Hugo is very flexible and allows us setting up our site easily as we want. In this web, I have a baseof.html file as template for the site. Inside this file I have a partial (an include in Hugo) file, head.html.


<!doctype html>
<html lang="es">
<head>
    {{ partial "head.html" . }}
</head>
<body></body>
</html>

We import styles.css file from css folder and call absURL Hugo function, so it will return us the absolute path file.

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{{ $css := "css/styles.css" | absURL }}
<link rel="stylesheet" href="{{ $css }}">

Static files in Hugo are saved by default in static folder so that is the first step, having styles.css in /static/css/. As this file is going to be generated, what I have done has been to create a folder called static-src where to have sources of my static files that I will subsequently process.

Before continuing, we will install the dependencies that we will need with npm.

npm install tailwindcss --save
npm install postcss postcss-cli @fullhuman/postcss-purgecss autoprefixer watch cross-env cssnano --save-dev

Let´s see what are these dependencies:

  • tailwindcss: CSS framework.
  • postcss postcss-cli: Tailwind uses PostCSS to get compiled and also we will add some plugins to CSS process.
  • @fullhuman/postcss-purgecss: Tailwind generates thousands of utility classes, most of which we probably don´t use. We will use this plugin for removing CSS that we are not actually using in our project.
  • autoprefixer: Optional, prefix for our stylesheets.
  • watch: Node package which allows us running tasks when any file is modified. It includes a cli that we will use on npm script.
  • cross-env: Package that allow us setting and using environment variables across platforms. NODE_ENV in our case.
  • cssnano: CSS minifier.

With all these dependencies we are ready to generate a CSS file with just the utility classes used and not the whole framework. Let´s generate an empty Tailwind config file so that we can customize default settings in the framework. We simply run:

npx tailwind init

A tailwind.config.js file will be generated:

module.exports = {
  theme: {
    extend: {}
  },
  variants: {},
  plugins: []
}

Finally we can create our /static-src/styles.css file invoking Tailwind directives. Besides, we can add custom styles we want, using Tailwind or not.

@tailwind base;
@tailwind components;

/* Custom CSS */
a.tag {
    @apply bg-gray-200 rounded-full px-3 py-1 font-semibold text-gray-700;
}
a.tag:hover {
    @apply underline;
}
/* Custom CSS end */

@tailwind utilities;

Tailwind is compiled using PostCSS, so adding new plugins is very easy. We are going to add autoprefixer in order to auto generate prefixes for CSS properties that need them. Besides when compiling for production, we will use purgecss and cssnano for reducing the file size for our stylesheet. Let´s see PostCSS config file (postcss.config.js in the root folder):

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: ['../../content/**/*.md', './layouts/**/*.html'],
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
});

module.exports = {
    plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
        ...(process.env.NODE_ENV === 'production' ? [purgecss, require('cssnano')] : [])
    ]
};

When using purgecss, we will search for files with .md extension inside content folder and .html in layouts. If you are working with javascript files or any other paths where you add CSS classes, you will have to adjust the array of paths to suit your needs. In this snippet of code we are considering that we are working with a theme inside /themes/themename/ folder. If we are working directly in layouts folder (as I do in this blog), we should adjust the paths:

const purgecss = require('@fullhuman/postcss-purgecss')({
    content: ['./content/**/*.md', './layouts/**/*.html', './static/**/*.js'],
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
});

With all this we can generate our npm scripts in package.json file:

{
  "name": "hugo-tailwindcss",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "css:watch": "node_modules/.bin/watch \"npm run css:build:dev\" ./static-src/css",
    "css:build:dev": "cross-env NODE_ENV=development npm run css:build",
    "css:build:prod": "cross-env NODE_ENV=production npm run css:build",
    "css:build": "node_modules/.bin/postcss static-src/css/styles.css -o static/css/styles.css"
  },
  "author": "",
  "license": "MIT",
  "dependencies": {
    "tailwindcss": "^1.1.4"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^2.0.5",
    "autoprefixer": "^9.7.4",
    "cross-env": "^6.0.3",
    "cssnano": "^4.1.10",
    "postcss": "^7.0.26",
    "postcss-cli": "^7.1.0",
    "watch": "^1.0.2"
  }
}

There are 3 tasks:

  • css:build:dev: Compile CSS in development mode, for greater agility we will have all Tailwind utilities by default without need to minimize and purge classes.
  • css:build:prod: Compile, purge and minify CSS file in order to get optimized stylesheet for production.
  • css:watch: Watch ./static-src/css folder. If it detects any change, it compiles CSS file again. It avoids tediously launching manually css:build:dev task on each change we do in our CSS.

With these tasks we can finally generate our stylesheet. I have created a Github repository with a theme which incorporates these scripts as a starting point to create your own theme for Hugo. You can try it out with some example content that I have created in other repository.

NOTE: It is possible to integrate the whole process thanks to Hugo pipe postCSS. You need to install globally postcss-cli in addition to any other plugins you use. I personally prefer not having global packages installed and being able to customize the process, but it´s an option that Hugo gives us.