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
- Why this boring stack is interesting
- Where templ fits
- A tiny HTMX example
- Three ways to add Tailwind
- The standalone setup
- The commands you will actually use
- Example repo
- 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.
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>
}
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
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
or you can just install globally and use everytime:
go install github.com/a-h/templ/cmd/templ@latest
templ generate
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>
}
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)
}
})
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>
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
Or with pnpm:
pnpm add -D tailwindcss @tailwindcss/cli
pnpm exec tailwindcss -i ./views/css/styles.css -o ./public/styles.css --watch
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
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
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";
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
For a production build:
tailwindcss -i ./views/css/styles.css -o ./public/styles.css --minify
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 .
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

Top comments (0)