Reading List

How to use Tailwind CSS in your Go programs from Christine Dodrill's Blog RSS feed.

How to use Tailwind CSS in your Go programs

hero image grass
Nikon D3300, Holga Lens, Photo by Xe Iaso -- A dithered picture of grass, vignetted around the edges. The palette is forced to be very natural.

When I work on some of my smaller projects, I end up hitting a point where I need more than minimal CSS configuration. I don't want to totally change my development flow to bring in a bunch of complicated toolkits or totally rewrite my frontend in React or something, I just want to make things not look like garbage. Working with CSS by itself can be annoying.

Aoi is coffee
<Aoi> Heck yes it is! I don't like working with CSS because it never really feels like I'm making progress. I'm always fighting with it. It's just left me with the impression that I'm just fundamentally bad at frontend work.
Numa is happy
<Numa> TBH, you're not bad at frontend work. You're bad at design. The way you get better at design is by doing more attempts at it. You can't get better at design by avoiding it. I'm not saying you have to be a designer. I'm saying you have to be willing to try.

Remember: ignorance is the default state.
Aoi is wut
<Aoi> You know, I never really thought about it like that. I guess I'll give it a shot.

I've found a way to make working with CSS a lot easier for me. I've been starting to use Tailwind in my personal and professional projects. Tailwind is a CSS framework that makes nearly every CSS behavior its own utility class. This means that you can make your HTML and CSS the same file, minimizing context switching between your HTML and CSS files. It also means that you can build your own components out of these utility classes. Here's an example of what it ends up looking like in practice:

<div class="bg-blue-500 text-white font-bold py-2 px-4 rounded">Button</div>

This is a button that has a blue background, white text, is bold, has a padding of 2, and has a rounded border. This looks like a lot of CSS to write for a button, but it's all in one place and can be customized for every button. This is a lot easier to work with than having to context switch between your HTML and CSS files.

Aoi is wut
<Aoi> What's that unit in px-2, it's padding on the X axis by two what?
Mara is happy
<Mara> It's rem, which is about 16 pixels (assuming you don't change the font size). The exact size of rem units can be confusing at first, but you end up internalizing it over time. Think about it like this: pr-1 is about the size of a space between words.

One of the biggest downsides is that Tailwind's compiler is written in JavaScript and distributed over npm. This is okay for people that are experienced JavaScript Touchers, but I am not one of them. Usually when I see that something requires me to use npm, I just close the tab and move on. Thankfully, Tailwind is actually a lot easier to use than you'd think. You really just have to install the compiler (either with npm or as a Nix package) and then run it. You can even set up a watcher to automatically rebuild your CSS file when you change your HTML templates. It's a lot less overhead than you think.

Assumptions

To make our lives easier, I'm going to assume the following things about your project:

  • It is written in Go.
  • You are using html/template for your HTML templates.
  • You have a static folder that has your existing static assets (eg: https://alpinejs.dev for interactive components).

Cadey is enby
<Cadey> I can't reccomend Alpine.js enough. It's everything I want out of progressive JavaScript enhancement of websites. Combined with Tailwind it is a killer combination. Read more about the combination here.

Setup

If you are using Nix or NixOS

Add the tailwindcss package to your flake.nix's devShell:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { self, nixpkgs }: {
    devShell = nixpkgs.mkShell {
      nativeBuildInputs = [ self.nixpkgs.tailwindcss ];
    };
  };
}

Then you should be able to use the tailwindcss command in your shell. Ignore the parts about installing tailwindcss with npm, but you may want to use npm as a script runner or to install other tools. Any time I tell you to use npx tailwindcss, just mentally replace that with tailwindcss.

First you need to install Tailwind's CLI tool. Make sure you have npm/nodejs installed from the official website.

Then create a package.json file with npm init:

npm init

Once you finish answering the questions (realistically, none of the answers matter here), you can install Tailwind:

npm install --dev --save tailwindcss

Now you need to set up some scripts in your package.json file. You can do this by hand, or you can use npm's built-in script runner to do it for you. This lets you build your website's CSS with commands like npm run build or make your CSS automatically rebuild with npm run watch. To do this, you need to add the following to your package.json file:

{
  // other contents here, make sure to add a trailing comma.
  "scripts": {
    "build": "tailwindcss build -o static/css/tailwind.css",
    "watch": "tailwindcss build -o static/css/tailwind.css --watch"
  }
}

Mara is hacker
<Mara> It's helpful to run npm run watch in another terminal while you're working on your website. This will automatically rebuild your CSS file when you change your HTML templates.

Next you need to make a tailwind.config.js file. This will configure Tailwind with your HTML teplate locations as well as let you set any other options. You can do this by hand, or you can use Tailwind's built-in config generator:

npx tailwindcss init

Then open it up and configure it to your liking. Here's an example of what it looks like when using Iosevka Iaso:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./tmpl/*.html"], // This is where your HTML templates / JSX files are located
  theme: {
    extend: {
      fontFamily: {
        sans: ["Iosevka Aile Iaso", "sans-serif"],
        mono: ["Iosevka Curly Iaso", "monospace"],
        serif: ["Iosevka Etoile Iaso", "serif"],
      },
    },
  },
  plugins: [],
};

Mara is hacker
<Mara> If you're using Iosevka Iaso fonts, make sure to import them in your base/header HTML template:

<link rel="stylesheet" href="https://cdn.xeiaso.net/static/pkg/iosevka/family.css" />

If you aren't serving your static assets in your Go program already, you can use Go's standard library HTTP server and go:embed:

//go:embed static
var static embed.FS

// assuming you have a net/http#ServeMux called `mux`
mux.Handle("/static/", http.FileServer(http.FS(static)))

This will bake your static assets into your Go binary, which is nice for deployment. Things you can't forget are a lot more robust than things you can forget.

Finally, add a //go:generate directive to your Go program to build your CSS file when you run go generate:

//go:generate npm run build

When you change your HTML templates, you can run go generate to rebuild your CSS files.

Mara is hacker
<Mara> Running go generate manually like this isn't as robust as you'd get when using build.rs to automagically regenerate it at compile time, but this can be mostly fixed by having a CI check make sure that all generated files are up to date. I wish we could have nice things.

Finally, make sure you import your Tailwind build in your HTML template:

<link rel="stylesheet" href="/static/css/tailwind.css" />

Now you can get started with using Tailwind in your HTML templates! I hope this helps.