DLM Tutorials

Setting up TailwindCSS with PostCSS in Hugo

Initial Setup

First, make a new hugo app:

hugo new site hugotails

Next, create a new theme

hugo new theme tailwind-theme

After we create the theme, we need to tell hugo to use it. To do this, let’s update our config.toml file like so:

baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"
theme = "tailwind-theme"

After the new theme is created and we’ve told hugo to use it, we’ll want to initialize an npm project so we can add our dependencies for managing the css.

Adding JavaScript Dependencies

npm init -y

Now we want to actually add the dependencies:

"devDependencies": {
  "autoprefixer": "^10.1.0",
  "postcss": "^8.2.1",
  "postcss-cli": "^8.3.1",
  "postcss-import": "^13.0.0",
  "tailwindcss": "^2.0.2"

These are the versions as of this writing. You can install current versions yourself by running

npm i --save-dev autoprefixer postcss postcss-cli postcss-import tailwindcss

Configuring Tailwindcss with postCSS

With the dependencies installed, we’ll want to set up initial configuration. For this, we’ll need to create a couple of directories inside of our theme folder. Then, we’ll add 3 files.

mkdir -p themes/tailwind-theme/assets/css
touch themes/tailwind-theme/assets/css/postcss.config.js
touch themes/tailwind-theme/assets/css/styles.scss


In the postcss.config.js, we’ll need to configure our plugins and tell postcss where the theme directory is. We also want to instruct postcss to use the custom tailwind configurations that we add to our tailwind.config.js later on. For now, we’re just going to create this file and leave it empty, but we’ll definitely be adding to it later. You can read more about how to customize tailwind in the offical docs.

// themes/tailwind-theme/assets/css/postcss.config.js
const themeDir = __dirname + '/../../';

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


Next, we’ll import the base, components and utilities modules from tailwindcss in the styles.scss file:

/* themes/tailwind-theme/assets/css/styles.scss */
@import "node_modules/tailwindcss/base";
@import "node_modules/tailwindcss/components";
@import "node_modules/tailwindcss/utilities";


Finally, we’ll finish out the configuration process by adding in the (currently) empty boilerplate for our tailwind.config.js file.

// themes/tailwind-theme/assets/css/tailwind.config.js
module.exports = {
  theme: {
    extend: {
  variants: {

  plugins: []

Testing out Tailwind in our Hugo Templates

To get this working in a way where we can see it in action in our hugo server, we have a few more tasks to complete. I’ll talk about a potential gotcha that may come up for newcomers to hugo at the end of this section. First, let’s take a look at the files we’ll be changing to demonstrate how we add tailwind to our existing hugo templates.


In this file, we need to tell hugo about our styles.scss file. We also want to make sure it’ gets converted to css and then piped through our postcss configuration before being added to the head tag in our html. There’s also some conditional logic here indicating that we should minify, fingerprint and run post processing on the stylesheet if we’re not using the server currently (it’s a production build).

  {{ $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 }}


In order to see the effect of the changes we make to our styles by adding tailwind, we’ll need to add some tailwind classes to one of our templates. Here, we’ll just add some styling to the included header.html template.

<!-- themes/tailwind-theme/layouts/partials/header.html -->
<header class="w-full bg-blue-300 py-4">
  <h1 class="text-center">Welcome to HugoTails!</h1>

We’re nearing the moment where we’ll be able to see in the browser whether this is working if we run hugo server -D --watch (runs our hugo server in development mode and watches for changes in order to rebuild the site and reload the server when we make changes to our code.)

If we do this now, we’ll actually see a blank screen, even though we’ve added this nice header with a blue background. This is the gotcha I referred to earlier and it has to do with another file we need to take a look at.


If you’re new to hugo and theme development, there’s a particular part of the code in this file that can cause an issue that I got stuck on for a bit. I’ll point it out to you here. By default, your new Hugo theme will have the following file at this location: themes/tailwind-theme/layouts/_default/baseof.html. The only change I’ve made to it is adding a couple of styles to the body tag to give it a dark mode feel.

<!-- themes/tailwind-theme/layouts/_default/baseof.html -->
<!DOCTYPE html>
  {{- partial "head.html" . -}}
  <body class="bg-gray-800 text-gray-50">
    {{- partial "header.html" . -}}
    <div id="content">
    {{- block "main" . }}{{- end }}
    {{- partial "footer.html" . -}}

This baseof.html template file is used as the outer shell for every html document generated by the hugo command. As such, it’s a great place to put any code that you want to be accessible anywhere on your site. Things like styles and scripts are potential candidates here. Also, markup you want to be present everywhere like a header that presents navigation or a footer with company information might find a nice home here.

For beginners, one thing to note is this line.

{{- block "main" . }}{{- end }}

The reason I’m drawing attention here is that having a block within baseof.html like this means that our templates need to define main if hugo is going to actually make use of baseof.

For example, if you have just a bit of html in your index.html file:

<!-- themes/tailwind-theme/layouts/index.html -->

Then when you visit the home page, you’re just going to see the Hello <h1> tag. You won’t see the header with the blue background that we added to the head.html partial.

Hello alone If we want our templates to work properly, they need to define "main" within them so that baseof can be applied as the shell and the template content can be added properly.

{{ define "main" }}
{{ end }}

Now, when we load up the home page in the browser, we see that our head partial is now loading the tailwind styles.

Hello with Tailwind Styles applied

You’ll notice that after we’ve done this, the Hello header is no longer large like it was before tailwind styles were applied.

Adding plugins to tailwind.config.js

Here is a case where you can test out that the tailwind.config.js configuration is working properly. Let’s head over there and add some theme configuration for the header sizes.

// themes/tailwind-theme/assets/css/tailwind.config.js
const plugin = require('tailwindcss/plugin')

module.exports = {
  theme: {
    extend: {

  variants: {

  plugins: [
    plugin(function({ addBase, config }) {
        'h1': { 
          fontSize: config('theme.fontSize.3xl'), 
          marginTop: config('theme.spacing.6'),
          marginBottom: config('theme.spacing.6'),
          fontWeight: config('theme.fontWeight.semibold')
        'h2': { 
          fontSize: config('theme.fontSize.2xl'), 
          marginTop: config('theme.spacing.4'),
          marginBottom: config('theme.spacing.4'),
          fontWeight: config('theme.fontWeight.semibold')
        'h3': { 
          fontSize: config('theme.fontSize.xl'), 
          marginTop: config('theme.spacing.3'),
          marginBottom: config('theme.spacing.3'),
          fontWeight: config('theme.fontWeight.semibold') 
        'h4': {
          fontSize: config('theme.fontSize.lg'), 
          marginTop: config('theme.spacing.2'),
          marginBottom: config('theme.spacing.2'),
          fontWeight: config('theme.fontWeight.semibold')

And after changing this configuration, let’s open up the browser again and take a look at what we’ve got.

With tailwind config for header font size

If you want to use hugo’s built in stylesheets for syntax highlighting. You can run a command like this:

hugo gen chromastyles --style=solarized-dark256 > themes/tailwind-theme/assets/css/syntax.css

Then, to load these styles, you’ll want to update the head.html partial. In this case, we’re setting this up so that any additional .css or scss files within the assets/css directory will be loaded as well and concatenated into a single file and then run through postCSS.

<!-- themes/tailwind-theme/partials/head.html -->
  {{ $scss := resources.Match "css/**.scss" | resources.Concat "css/compiled.css" | toCSS }}
  {{ $css := resources.Match "css/**.css" | resources.Concat "css/styles.css" }}
  {{ $bundle := slice ($scss) ($css) | resources.Concat "css/bundle.css" | postCSS (dict "config" "./assets/css/postcss.config.js") }}

  {{ if .Site.IsServer }}
  <link rel="stylesheet" href="{{ $bundle.RelPermalink }}">
  {{ else }}
  {{ $bundle := $bundle | minify | fingerprint | resources.PostProcess }}
  <link rel="stylesheet" href="{{ $bundle.Permalink }}" integrity="{{ $bundle.Data.Integrity }}">
  {{ end }}

Adding a Paperclip Icon to Code Blocks To Enable Copying the Contents

I wanted to add a paperclip icon to the code blocks so the whole content can be copied with a click. I wanted to keep the ability for users to copy just part of the code block if they’d prefer. So you can still highlight part of the block and copy that if you like, but if you click on the paperclip, it will copy the entire contents of the code block to your clipboard.

In order to do this, we actually have to grab the innerText of the pre tag first and then create a textarea, fill it with the contents of the pre, put it into the pre (just briefly so it’s in the DOM) and then select the contents of the textarea and invoke document.execCommand('copy'). After this is complete, we’re removing the textarea. Currently, it also changes the paperclip color to green and switches it back to white after 3 seconds have elapsed.

// themes/tailwind-theme/assets/js/components/paperclip.js
export default class PaperClip {
  constructor(pre) {
    this.pre = pre;

  copy() {
    let code = document.createElement('textarea');
    code.value = this.pre.textContent.trimEnd();
    setTimeout(() => this.changeClipColor('white'), 3000);


  changeClipColor(color) {
    this.path ||= this.div.querySelector('path')
    this.path.setAttribute('fill', color);
  render() {
    this.div = document.createElement('div');
    this.div.classList.add(..."absolute top-0 right-3".split(' '))
    this.div.innerHTML = `
        version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
        width="18px" height="18px" viewBox="0 0 950 950" style="enable-background:new 0 0 950 950; fill: 'white'; stroke: 'white';" xml:space="preserve"
        class="absolute top-2 right-0"
        <path d="M857.7,141.3c-30.1-30.1-65.1-53.5-104.3-69.4c-37.8-15.3-77.7-23.2-118.7-23.2c-40.9,0-80.9,7.7-118.7,22.9
          C911.101,206.3,887.8,171.3,857.7,141.3z" stroke="white" fill="white" class="transition duration-300 ease-out" />
    this.div.onclick = () => this.copy();
    return this.div;

Finally, to make sure that these paperclips get added on page load, we add an event listener that will get all of the pre tags in the document after the DOMContentLoaded event. With those pre’s, we’ll create a new Paperclip instance for each, adding the paperclip icon to the pre and a click event listener to it that will trigger the copy() method.

// themes/tailwind-theme/assets/js
import PaperClip from './components/paperclip';
document.addEventListener('DOMContentLoaded', (e) => {
  document.querySelectorAll('pre').forEach(pre => new PaperClip(pre));

Keep working in the woodshed until your skills catch up to your taste.
If you'd like to get in touch, reach out on LinkedIn.