Building a Complex HTMX + Go + Tailwind + Template App

A multi-page project demonstrating advanced Go templates, HTMX interactions, and Tailwind styling.

1. Introduction

In this tutorial, we’ll build a multi-page Go application that uses:

  • Go templates with a layout and partials
  • HTMX for three distinct interactions:
    1. Pressing a button to display a Tailwind-styled element with server-provided data (e.g., {{.Username}}).
    2. Switching between two different button styles/elements (toggle) using hx-target.
    3. Using range in a Go template to display multiple repeated snippets.
  • Tailwind CSS for more advanced styling

We won’t discuss why you might choose these technologies — we’ll simply show how to integrate them for a richer user experience.

2. Project Layout

Below is one suggested folder structure:

my-complex-htmx/
├── go.mod
├── main.go
└── templates/
    ├── base.html          (layout file)
    |── header.html
    │── footer.html
    ├── index.html         (home page)
    ├── custombutton.html  (button snippet w/ .Username)
    ├── button1.html       (first toggle state)
    ├── button2.html       (second toggle state)
    └── range.html         (demonstrate {{range .}})
        

- base.html defines a main layout block.
- header.html and footer.html hold shared elements.
- index.html is the home page, referencing the layout.
- custombutton.html is for the button example that reveals server-sent data.
- button1.html / button2.html are toggled by HTMX.
- range.html uses range to repeat a snippet multiple times.

3. Layout & Partial Templates

base.html

            <!-- CODE BLOCK -->
            {{define "base"}}
            <!doctype html>
            <html>
                <head>
                    <meta charset="UTF-8" />
                    <title>{{block "title" .}}My Complex HTMX App{{end}}</title>
                    <!-- Tailwind & HTMX -->
                    <link
                        href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.1/dist/tailwind.min.css"
                        rel="stylesheet"
                    />
                    <script src="https://unpkg.com/htmx.org@1.9.2"></script>
                </head>
                <body class="bg-gray-900 text-teal-100 font-sans">
                    {{template "partials/header.html" .}}

                    <main class="container mx-auto px-4 py-8">
                        {{block "content" .}}{{end}}
                    </main>
                </body>
            </html>
            {{end}}

        

This layout includes Tailwind (via CDN) and HTMX. The main content block is injected with {{block "content" .}}, while header and footer partials are pulled in with {{template "partials/header.html" .}} and {{template "partials/footer.html" .}}.

partials/header.html

            <!-- CODE BLOCK -->
            {{define "partials/header.html"}}
            <header class="text-center py-4 border-b border-teal-700 mb-4">
                <h1 class="text-2xl font-bold">My Complex HTMX Example</h1>
                <script src="https://unpkg.com/@tailwindcss/browser@4"></script>
                <script src="https://unpkg.com/htmx.org@2.0.4"></script>
            </header>
            {{end}}

        

partials/footer.html

            <!-- CODE BLOCK -->
            {{define "partials/footer.html"}}
            <footer class="text-center py-4 border-t border-teal-700 mt-4">
                <p class="text-sm">&copy; 2025 MyComplexHTMXApp. All rights reserved.</p>
            </footer>
            {{end}}

        

4. Home Page (index.html)

Our home page extends base.html. We add links/buttons that route the user to different examples or triggers an HTMX call. This is just a placeholder to get started.

    <!-- CODE BLOCK -->
    {{define "base"}}
    <!doctype html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>{{block "title" .}}My Complex HTMX App{{end}}</title>
            <!-- Tailwind & HTMX -->
            <link
                href="https://cdn.jsdelivr.net/npm/tailwindcss@3.3.1/dist/tailwind.min.css"
                rel="stylesheet"
            />
            <script src="https://unpkg.com/htmx.org@1.9.2"></script>
        </head>
        <body class="bg-gray-900 text-teal-100 font-sans">
            {{template "partials/header.html" .}}

            <main class="container mx-auto px-4 py-8">
                {{block "content" .}}{{end}}
            </main>
        </body>
    </html>
    {{end}}


5. Custom Button w/ Tailwind and HTMX

When the user presses a button, an HTMX request fetches a Tailwind-styled snippet that includes {{.Username}}. Let's define custombutton.html as the snippet we’ll return from the server.

<!-- CODE BLOCK -->
{{define "custombutton"}}
<div class="bg-teal-800 text-white p-4 rounded shadow">
    <p class="text-lg font-bold">Hello, {{.Username}}!</p>
    <p>This element was loaded from the server via HTMX.</p>
</div>
{{end}}

        

6. Toggling Between Two Elements

We want two HTML snippets (button1.html and button2.html) that swap when clicked. Each snippet has a button pointing to the opposite route.

            &#60;!-- CODE BLOCK --&#62;
            {{define "partials/footer.html"}}
            &#60;footer class="text-center py-4 border-t border-teal-700 mt-4"&#62;
                &#60;p class="text-sm"&#62;&#38;copy; 2025 MyComplexHTMXApp. All rights reserved.&#60;/p&#62;
            &#60;/footer&#62;
            {{end}}


        
            <!-- CODE BLOCK -->
            {{define "button2"}}
            <button
                hx-get="/toggle1"
                hx-swap="outerHTML"
                class="bg-green-700 hover:bg-green-600 text-white px-4 py-2 rounded m-8"
            >
                I'm BUTTON2. Click to swap!
            </button>
            {{end}}

        

7. Using Go's {{range .}} Operator

Suppose we want to show multiple repeated snippets. Let's define range.html like this, using {{range .Items}} to iterate over a slice from Go:

<!-- CODE BLOCK -->
{{define "rangexample"}}
<div>
    <h2 class="text-xl font-semibold mb-2">Range Example</h2>

    <div class="bg-stone-800 rounded p-3 mb-2">
        {{range . }}
        <p class="text-sm text-stone-300">Username: {{ .Username }}</p>
        <p class="text-sm text-stone-300">Item: {{ .Items }}</p>
        {޾nd}}
    </div>
</div>
{{end}}

        

8. Another {{range .}} example - array of elements

Suppose we want to show a snippet where one thing stays the same, and everything else changes. Let's define rangesecond.html like this, using {{range .Items}} to iterate over another slice.:

              <!-- CODE BLOCK -->
              {{define "secondrange"}}
              <div>
                  <h2 class="text-xl font-semibold mb-2">Range Example</h2>

                  <div class="bg-stone-800 rounded p-3 mb-2">
                      <p class="text-sm text-stone-300">Username: {{ .Username }}</p>
                      {{ range .Items }}
                      <p class="text-sm text-stone-300">Item: {{ . }}</p>
                      {{ end }}
                  </div>
              </div>
              {{end}}

          

9. The Go Server (main.go)

Next, let’s show how to wire up these routes. Notice we have multiple handlers:

// code block
package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
)

type UserData struct {
	Username string
	Items    string
}

var tmpl *template.Template

func main() {
	var err error
	// Parse all .html files SUPER COMPLICATED GLOB PARSE
	//Template matching is simple in Go, otherwise we'd have to use regex like: ("templates/[A-Za-z0-9]+/*[A-Za-z0-9]+(/[*])?/?.[A-Za-z0-9]+")
	tmpl, err = template.ParseGlob("templates/*.html")
	if err != nil {
		log.Fatalf("Error parsing templates: %v", err)
	}

	// Routes
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/custombutton", customButtonHandler)
	http.HandleFunc("/toggle", toggleHandler)

	// For toggling:
	http.HandleFunc("/toggle1", toggle1Handler)
	http.HandleFunc("/toggle2", toggle2Handler)

	http.HandleFunc("/range", rangeHandler)
	http.HandleFunc("/extrarange", extrarangeHandler)

	fmt.Println("Server running at http://localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
		//4 hours(!)
	}
}

// 1) Home page
func indexHandler(w http.ResponseWriter, r *http.Request) {
	data := UserData{Username: "Guest"}
	if err := tmpl.ExecuteTemplate(w, "index", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// 2) Custom Button route
func customButtonHandler(w http.ResponseWriter, r *http.Request) {
	data := UserData{Username: "Alice"} // The name we inject
	if err := tmpl.ExecuteTemplate(w, "custombutton", data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// 3) Toggle route
func toggleHandler(w http.ResponseWriter, r *http.Request) {
	// Return an initial button (button1)
	if err := tmpl.ExecuteTemplate(w, "button2", nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func toggle1Handler(w http.ResponseWriter, r *http.Request) {
	// Return button2 snippet
	if err := tmpl.ExecuteTemplate(w, "button1", nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func toggle2Handler(w http.ResponseWriter, r *http.Request) {
	// Return button1 snippet
	if err := tmpl.ExecuteTemplate(w, "button2", nil); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// 4) Range route
func rangeHandler(w http.ResponseWriter, r *http.Request) {
	data2 := UserData{
		Username: "TestUser2",
		Items:    "B",
	}
	data1 := UserData{
		Username: "TestUser1", // It orders itself. Testuser1 will appear ahead of 2
		Items:    "A",
	}
	data3 := UserData{
		Username: "TestUser3",
		Items:    "C",
	}

	netdata := []UserData{data1, data2, data3}

	if err := tmpl.ExecuteTemplate(w, "rangexample", netdata); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

}

func extrarangeHandler(w http.ResponseWriter, r *http.Request) {
	//Another example - this time with a list of items but with the user being the same

	type NewUserData struct {
		Username string
		Items    []string
	}

	finaldata := NewUserData{
		Username: "I_Got_Items!!",
		Items:    []string{"Water", "Bread", "Fish"},
	}

	if err := tmpl.ExecuteTemplate(w, "secondrange", finaldata); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	fmt.Printf("Range is read!")
}
//end code slice
        

9. Running & Testing

1. In your project folder, run go mod init to initialize the go code. This should create a go.mod with "module main" at the top
2. Run go run main.go to start the server.
3. Visit http://localhost:8080 in your browser.
4. Click on the links to test:

10. Conclusion

By combining Go templates (with a layout + partials), HTMX for dynamic requests, and Tailwind for styling, you can build a multi-page site that feels both modern and well-structured. Each snippet or page can independently use Tailwind’s utility classes for consistent styling, while HTMX updates parts of the DOM with minimal fuss. Enjoy building your advanced GOTTH stack!

You did it.

I have nothing more to teach you (for now). Go out and make mistakes!