Gomponents & TailwindCSS

I know it is polarising, but I do not have a very strong opinion on TailwindCSS. Attaching a few dozen classes to a div to make something work (instead of writing CSS yourself) surely is not elegant… but it works. And with their Plus package even I can build a decently looking interface without too much trouble. So far I have enjoyed working with it, but if someone would tell me "we don't use TW for the new project" I would also not waste five minutes to argue about it.

Gomponents allows you to write HTML in Go. If you think this sounds horrible you capture the first two hours using it. It looks wrong. It feels wrong to write. But once it clicks it is not just fun but also pretty neat! Not only can I test my templates easily, I can literally do all the fun stuff I can do in Go while rendering my template.

Now to the really fun part: Combining TailwindCSS and Gomponents. I can say for sure I rarely was this efficient in bootstrapping new UI screens than with this combination, once basic components have been built.

The Button

Assuming a button can have three states. Basic, primary and danger. We define the CSS classes as constants.

const (
    buttonBase    = "rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
    buttonPrimary = "bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:outline-indigo-600"
    buttonDanger  = "bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600"
)

Assuming we want a link to look like a button (this is a lazy example, let us not go into details if this is a good idea or not)

func LinkButton(url, text string, primary, danger bool) Node {
    return A(
        Href(url),
        Text(text),
        Classes{
            buttonBase:    true,
            buttonDanger:  danger,
            buttonPrimary: primary,
        },
    )
}

We pass the url and the display text as well as an indicator if this is a primary or danger styled button. All we have to do to render a button on the page - including all classes and globally changeable with updating our constants

lb := LinkButton("awesome.tld", "Awesome", false, true)

Grid

Most of the app I am working on using a two column layout. So I have a helper function to build the grid to which I simply pass the size of the columns and the content for the left and right side.

func GridTwo(leftWidth, rightWidth int, leftContent, rightContent Node) Node {
    return Div(
        Class("grid grid-cols-12 "+gridGap),
        Div(
            Class("col-span-"+strconv.Itoa(leftWidth)),
            leftContent,
        ),
        Div(
            Class("col-span-"+strconv.Itoa(rightWidth)),
            rightContent,
        ),
    )
}

While this is not the most impressive example imagine having a few flexbox classes in there to align the right side, proper padding,...

But if I use…

Yes, you can absolutely do this with most templating languages as well. Gomponents is not very special when it comes to this kind of building out components. But being able to unit test the templates and not fighting the useless monstrosities template engines in Go means I am really happy with this solution.

Sprinkle in a bit HTMX and a function to render a template and things look pretty clean for a large application.

func (t *Template1) Render(w io.Writer) error {
    if t.filters.Htmx {
        if t.filters.Target == sectionA {
            return t.secA().Render(w)
        } else if t.filters.Target == sectionB {
            return t.secB().Render(w)
        }
    }

    b := Div(components.GridTwo(6, 6,
            Div(ID(sectionA), t.secA()),
            Div(ID(sectionB), t.secB()),
        ),
    )
    return Page(b).Render(w)
}

Random advice

In examples and documentation you often see Gomponents being imported as a dot import

import (
    . "maragu.dev/gomponents"
    . "maragu.dev/gomponents/html"
)

Do not do this if you want to define your own components, use an aliased import.

import (
    g "maragu.dev/gomponents"
    h "maragu.dev/gomponents/html"
)

While this is less fun to type, you can declare H1 or A while applying styles without coming up with new names. This is a small inconvenience for building the components, but does not force you to remember every alias you gave an HTML element when building out a page.

Gomponents might look strange and not feel right when using it the first time, and TailwindCSS can be the same way. But combined it is the best time I had writing code that outputs HTML in Go so far!

posted on April 15, 2025, 7:03 p.m. in Tech Quips, golang