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!