DEV Community

Cover image for Go + templ + HTMX + Tailwind CSS v4 setup in 2026
Peter Ogbonna
Peter Ogbonna

Posted on

Go + templ + HTMX + Tailwind CSS v4 setup in 2026

I only wanted to style a Go application.

Somehow, I ended up reading tutorials that asked me to run commands Tailwind no longer supports, create configuration files Tailwind v4 no longer needs, and install Node.js for a project that otherwise had nothing to do with Node.js.

After enough dead ends, I found a setup I actually enjoy: Go renders the application, templ gives me typed HTML components, HTMX adds focused interactions, and Tailwind's standalone executable compiles the CSS. No frontend framework and no node_modules directory.

This is the guide I wish I had found.

Table of contents

  1. Why this boring stack is interesting
  2. Where templ fits
  3. A tiny HTMX example
  4. Three ways to add Tailwind
  5. The standalone setup
  6. The commands you will actually use
  7. Example repo
  8. References

Why this boring stack is interesting

I mean boring affectionately :)

The browser sends an HTTP request. Go runs ordinary application code. A templ component renders HTML. HTMX can place a returned fragment into the page. Tailwind compiles the utility classes it finds into a static stylesheet.

The flow

There is one main programming language, one server, and HTML remains the application boundary. You do not need a client-side state store just to submit a form or refresh a list.

This stack is especially pleasant for content sites, dashboards, admin tools, CRUD applications, and small products where most interactions naturally map to HTTP requests.

It is not the answer to everything. A browser-based design tool or highly offline application may benefit from a substantial client-side runtime. But many applications are much closer to “request data, change data, render HTML” than their architecture admits.

Where templ fits

Go already has html/template, and it is good. It is in the standard library, widely understood, and performs contextual escaping to produce HTML safely. You do not need templ because the standard library is broken.

I use templ because I prefer its development model for component-heavy interfaces and type system.

Here is a small component:

package views

templ Greeting(name string) {
    <section class="rounded-xl border border-slate-200 p-6">
        <h1 class="text-2xl font-bold">Hello, { name }.</h1>
        <p class="mt-2 text-slate-600">Your page came from Go.</p>
    </section>
}
Enter fullscreen mode Exit fullscreen mode

templ turns this into Go code. The component has a typed parameter, can call other components, and can use familiar Go control flow. Misspell a field or pass the wrong type and the compiler can catch the mistake before the page reaches a user.

The trade-off is code generation: after changing a .templ file, you generate its corresponding Go source.

go tool templ generate
Enter fullscreen mode Exit fullscreen mode

For Go 1.24 and newer, I prefer tracking the generator as a project tool:

go get github.com/a-h/templ
go get -tool github.com/a-h/templ/cmd/templ@latest
go tool templ generate
Enter fullscreen mode Exit fullscreen mode

or you can just install globally and use everytime:

go install github.com/a-h/templ/cmd/templ@latest
templ generate
Enter fullscreen mode Exit fullscreen mode

A tiny HTMX example

templ renders HTML, HTMX decides when a part of the page should ask the server for new HTML.
visit the setup, download the htmx.min.js and put in your public folder

templ GreetingPanel() {
    <section id="greeting">
        <button
            class="rounded-lg bg-emerald-800 px-4 py-2 text-white"
            hx-get="/greeting"
            hx-target="#greeting"
            hx-swap="outerHTML">
            Greet me
        </button>
    </section>
}

templ Greeting() {
    <section id="greeting" class="rounded-xl bg-emerald-50 p-5">
        <p class="font-semibold text-emerald-950">Hello from the server.</p>
    </section>
}
Enter fullscreen mode Exit fullscreen mode

The button means: send GET /greeting, take the HTML response and replace the element identified by #greeting.

http.HandleFunc("GET /greeting", func(w http.ResponseWriter, r *http.Request) {
    if err := views.Greeting().Render(r.Context(), w); err != nil {
        http.Error(w, "could not render greeting", http.StatusInternalServerError)
    }
})
Enter fullscreen mode Exit fullscreen mode

There is no JSON response to decode and no duplicate client-side component to keep in sync. The server returns the next piece of UI.

That is the useful boundary in this stack:

  • Go owns application state and decisions.
  • templ owns reusable HTML.
  • HTMX requests and swaps HTML fragments.
  • Tailwind owns presentation.

Three ways to add Tailwind

Path 1: the Play CDN

The fastest possible experiment is Tailwind's browser package:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
Enter fullscreen mode Exit fullscreen mode

Add that to the page and Tailwind classes work without a build command.

While the CDN approach is extremely simple, you may want to consider not using CDNs in production.

That article discusses broader privacy, security, reliability, and performance trade-offs of third-party JavaScript CDNs. Tailwind's own documentation is more direct about this particular package stating that the Play CDN is designed for development, not production.

Path 2: npm or pnpm

The official package-manager route is entirely valid:

npm install --save-dev tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i ./views/css/styles.css -o ./public/styles.css --watch
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add -D tailwindcss @tailwindcss/cli
pnpm exec tailwindcss -i ./views/css/styles.css -o ./public/styles.css --watch
Enter fullscreen mode Exit fullscreen mode

This gives you normal dependency declarations and a lockfile, and updates fit the JavaScript package-management workflow. If the project already uses npm for other frontend tooling, this is a fair choice.

The downside in a Go only project is that Tailwind introduces an additional toolchain: Node.js, a package.json, a lockfile, install commands in CI, and a local node_modules directory. You normally ignore node_modules rather than commit it, but it still exists on each development machine and build environment.

None of that is catastrophic, it was simply machinery I did not otherwise need.

Path 3: The standalone executable (recommended)

Tailwind also publishes a self-contained CLI for Linux, macOS, and Windows. It performs the build ahead of time and emits an ordinary CSS file, without requiring Node.js or npm on your machine.

That is the path I use.

The Standalone Setup

1. Download the correct binary

Open the official Tailwind CSS releases page and choose the executable for your operating system and CPU and download.

2. Put it somewhere sensible

You have two good options.

Keep it inside the project, for example in ./bin/tailwindcss, and invoke that exact path:

./bin/tailwindcss -i ./views/css/styles.css -o ./public/styles.css --watch
Enter fullscreen mode Exit fullscreen mode

Or put it in a directory already on your PATH, so you can run tailwindcss anywhere.

Go developers often already have their Go tool directory on PATH. Find the location with:

go env GOPATH
Enter fullscreen mode Exit fullscreen mode

For Linux users that becomes $HOME/go/bin and %USERPROFILE%\\bin for windows users. Copying Tailwind executable there saves a separate PATH change only if that directory is already on your PATH, else add the folder to PATH

3. Create the Tailwind entry file

Here is the entire minimum Tailwind v4 entrypoint:

/* views/css/styles.css */
@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

It does not require tailwind.config.js or tailwindcss init for the basic setup. Tailwind v4 moved toward CSS-first configuration.

If you execute the build from your repository root, automatic source detection will usually find the class names in your .templ files and generate the plain css which you'll hook up to your <link /> tag

4. Compile the stylesheet

During development:

tailwindcss -i ./views/css/styles.css -o ./public/styles.css --watch
Enter fullscreen mode Exit fullscreen mode

For a production build:

tailwindcss -i ./views/css/styles.css -o ./public/styles.css --minify
Enter fullscreen mode Exit fullscreen mode

The browser receives /public/styles.css; it never knows or cares how that file was produced.

The commands you will actually use

Once everything is installed, the working loop is small:

# Generate Go code from .templ files once
go tool templ generate

# Regenerate templates as they change
go tool templ generate --watch

# Rebuild CSS as Tailwind classes change
tailwindcss -i ./views/css/styles.css -o ./public/styles.css --watch

# Run the Go application
go run .
Enter fullscreen mode Exit fullscreen mode

Run the watchers in separate terminals, or let a task runner coordinate them, a small Makefile is enough

The Tailwind watcher rebuilds CSS and it does not restart your Go process, templ provides its own reload workflow.

templ/tailwind rebuild, server restart, and browser reload are three different jobs.

I also put together a complete example repository containing this setup:

Browse the example Go + templ + HTMX + Tailwind v4 repository

References

Top comments (0)