How to use Tailwind CSS v3 in Hugo themes (including live reload)

·4 min·Andreas Haerter·

We redesigned foundata.com in August 2023 and now use the Static Site Generator Hugo. The theme developed for the site is using Tailwind CSS v3 as CSS framework.

But enough with corporate talk about the awesomeness of our website relaunch 😃, let me share some tips for Hugo theme developers who also want to use Tailwind CSS v3.x and its JIT-Compiler.

There are a lot of useful resources about Hugo and Tailwind CSS on the web.1 However, there is still little information about how to use Tailwind CSS v3.x and its JIT compiler when developing a Hugo theme. In particular, descriptions of how to setup a development mode with live reload for both are rarely found. I will therefore show how we implemented this in our Hugo template.2

Sample page including live reload on changes with hugo server

Hugo offers a built-in web server.. This is especially useful when developing a template, as it automatically injects liverelaod-js into the pages. So it does not only re-generate the pages when changes are made, but the browser also loads the new content automatically. As a developer, you can simply leave your browser open, save a change to the template or the example content and the page reloads directly. A nice development experience.

The content directory and the theme to be used can be defined using the parameters --source and --themesDir. This allows you to manage a complete sample website including configuration, which uses all the features of the theme and provides different text lengths etc. pp. to optimize the typography during development. We did this under exampleSite.

I use the following call to start the web server with our sample content and theme (an explanation of each parameter can be found in the documentation on hugo server) :

hugo server \
  --source ./exampleSite \
  --themesDir ../.. \
  --baseURL http://localhost/ \
  --port 1313 \
  --buildDrafts \
  --cleanDestinationDir \
  --logLevel debug \
  --disableFastRender \
  --gc \
  --minify \
  --noHTTPCache \
  --printI18nWarnings \
  --templateMetrics \
  --templateMetricsHints

Please note: --logLevel debug was introduced with v0.114.0. If your Hugo version is older, you have to use --debug instead.

Re-generate the Tailwind CSS styles on changes via JIT compiler

Tailwind CSS is typically installed via NodeJS. The npx tailwindcss command also provides file change monitoring (--watch parameter) with automatic style regeneration via JIT-Compiler.

The CSS source file of our Hugo theme is located at ./assets/styles/tw-main-src.css, the output file is generated at ./assets/styles/tw-main-dist.css. The following command is used for this:

NODE_ENV=development npx tailwindcss \
  -i ./assets/styles/tw-main-src.css \
  -o ./assets/styles/tw-main-dist.css \
  --watch --minify

The tailwind.config.js file defines which directories of the Hugo template should be searched for styles. In our template it looks like this:

//[...]
module.exports = {
  /* Content sources to tell Tailwind which files to scan for used class names for
     generating the needed CSS. More info:
     - https://tailwindcss.com/docs/content-configuration#configuring-source-paths
  */
  content: [
    "./layouts/**/*.html",
    "./content/**/*.{html,md}",
    "./themes/foundata/layouts/**/*.html",
    "./exampleSite/**/*.{html,md}",
  ],
  //[...]
}

In the file ./themes/foundata/layouts/_default/baseof.html the following lines are responsible for loading the generated CSS file:

  {{/* Use a Hugo scratch to store files and content for bundling CSS and JS assets */}}
  {{ $assets := newScratch }}

  {{/* TailwindCSS */}}
  {{ $assets.Add "css" (slice (resources.Get "styles/tw-main-dist.css")) }} {{/* output file for distribution */}}

  [...]

  {{/* Get the scratch's CSS content as bundled, optimized file */}}
  {{ if $assets.Get "css" }}
    {{ $bundleCSS := $assets.Get "css" | resources.Concat "styles/bundle.css" | resources.Minify | resources.Fingerprint "sha256") }}
    <link type="text/css" rel="stylesheet" href="{{ $bundleCSS.RelPermalink }}" integrity="{{ $bundleCSS.Data.Integrity }}">
  {{ end }}

Use everything together via packages.json

There is a packages.json in the root of our template as the Tailwind CSS installation is managed via NodeJS. It can also be used to comfortably merge the above commands.

We have placed them into individual scripts that can be called via npm run <name>. The npm-run-all package gives us the possibility to concatenate everything without deadlocks. In our packages.json, it looks like this:

//[...]
{
  "name": "hugo-foundata-theme",
  "version": "1.0.0",
  "description": "foundata theme for Hugo",
  "scripts": {
    [...]
    "dev": "npm-run-all --parallel dev:tailwindcss dev:example --print-name --race",
    "dev:tailwindcss": "touch ./assets/styles/tw-main-dist.css && NODE_ENV=development npx tailwindcss -i ./assets/styles/tw-main-src.css -o ./assets/styles/tw-main-dist.css --watch --minify",
    "dev:example": "hugo server --source ./exampleSite --themesDir ../.. --baseURL http://localhost/ --port 1313 --buildDrafts --cleanDestinationDir --disableFastRender --gc --minify --noHTTPCache --printI18nWarnings --templateMetrics --templateMetricsHints",
    "build:vendorlibs": "npm-run-all preinstall postinstall --print-name",
    "build:tailwindcss": "NODE_ENV=production npx tailwindcss -i ./assets/styles/tw-main-src.css -o ./assets/styles/tw-main-dist.css --minify"
  },
  //[...]
}

That’s it. To work on the theme, simply change into the theme directory, run npm run dev and access the Hugo example page at http://localhost:1313. It is automatically regenerated on every change to a style, template or sample content. It then gets automatically reloaded in the browser. This is particularly reasonable given Tailwind CSS’s utility-first approach.


  1. Besides the official documentation, I found https://www.regisphilibert.com/tags/hugo/ really helpful. ↩︎

  2. Hugo version v0.112.0 also introduced an option to integrate Tailwind CSS v3.x in a better way. But we take a slightly different approach. ↩︎