HTMX Integration
HTMX is at the heart of irgo's interactivity model. Instead of writing JavaScript to handle user interactions, you use HTML attributes to make requests and swap content.
The Hypermedia Pattern
The traditional SPA approach:
User Click → JavaScript → API Call → JSON Response → JavaScript → DOM UpdateThe HTMX/irgo approach:
User Click → HTMX Request → Go Handler → HTML Response → DOM SwapYour Go handlers return HTML fragments. HTMX swaps them into the page. No client-side rendering, no state synchronization, no JavaScript frameworks.
Core HTMX Attributes
Making Requests
<!-- GET request --><button hx-get="/api/users">Load Users</button><!-- POST request --><form hx-post="/api/users"> <input name="name" /> <button type="submit">Create</button></form><!-- PUT request --><button hx-put="/api/users/123">Update</button><!-- DELETE request --><button hx-delete="/api/users/123">Delete</button><!-- PATCH request --><button hx-patch="/api/users/123">Patch</button>Targeting Elements
<!-- Target by ID --><button hx-get="/users" hx-target="#user-list">Load</button><div id="user-list"></div><!-- Target this element --><div hx-get="/content" hx-target="this">Click me</div><!-- Target closest ancestor --><button hx-delete="/item/1" hx-target="closest .item">Delete</button><!-- Target with CSS selector --><button hx-get="/nav" hx-target="body > nav">Update Nav</button>Swap Strategies
<!-- Replace inner content (default) --><div hx-get="/content" hx-swap="innerHTML">Loading...</div><!-- Replace entire element --><div hx-get="/content" hx-swap="outerHTML">Replace me</div><!-- Append to end --><ul hx-get="/items" hx-swap="beforeend"> <li>Existing item</li></ul><!-- Prepend to start --><ul hx-get="/items" hx-swap="afterbegin"> <li>Existing item</li></ul><!-- Delete element (for DELETE requests) --><button hx-delete="/item/1" hx-swap="delete">Remove</button><!-- Insert after element --><div hx-get="/sibling" hx-swap="afterend">I'm first</div><!-- Insert before element --><div hx-get="/sibling" hx-swap="beforebegin">I'm last</div>Triggers
<!-- Click (default for buttons/links) --><button hx-get="/data" hx-trigger="click">Click me</button><!-- Change (default for inputs) --><input hx-get="/search" hx-trigger="change" name="q" /><!-- Keyup with delay (for search) --><input hx-get="/search" hx-trigger="keyup changed delay:300ms" name="q" /><!-- On load --><div hx-get="/data" hx-trigger="load">Loading...</div><!-- When revealed (infinite scroll) --><div hx-get="/more" hx-trigger="revealed">Load more</div><!-- Polling --><div hx-get="/status" hx-trigger="every 5s">Status: OK</div><!-- Intersection observer --><div hx-get="/lazy" hx-trigger="intersect once">Lazy load</div><!-- From another element --><input id="search" name="q" /><div hx-get="/results" hx-trigger="keyup from:#search delay:300ms"> Results here</div>Request Modifiers
Including Values
<!-- Include values from other elements --><input id="search" name="q" value="hello" /><button hx-get="/search" hx-include="#search">Search</button><!-- Include closest form --><form> <input name="name" value="John" /> <input name="email" value="john@example.com" /> <button hx-post="/users" hx-include="closest form">Submit</button></form><!-- Include all inputs with class --><button hx-post="/bulk" hx-include=".selected">Bulk Action</button>Request Headers
<!-- Add custom headers --><button hx-get="/api/data" hx-headers='{"X-Custom-Header": "value"}'> Load</button>Confirmation
<!-- Confirm before request --><button hx-delete="/users/123" hx-confirm="Are you sure you want to delete this user?"> Delete User</button>Response Headers (Server-Side)
Control HTMX behavior from your Go handlers:
func CreateUser(ctx *router.Context) (string, error) { user := createUser(ctx.FormValue("name")) // Redirect (HTMX-aware) ctx.Redirect("/users/" + user.ID) return "", nil}func UpdateUser(ctx *router.Context) (string, error) { user := updateUser(ctx.Param("id"), ctx.FormValue("name")) // Update browser URL without redirect ctx.PushURL("/users/" + user.ID) // Trigger client-side event ctx.Trigger("userUpdated") return renderer.Render(templates.UserCard(user))}func DeleteUser(ctx *router.Context) (string, error) { deleteUser(ctx.Param("id")) // Trigger event after swap completes ctx.TriggerAfterSwap("showNotification") return "", nil}Out-of-Band Swaps
Update multiple elements with a single response:
templ CreateUserResponse(user User, totalUsers int) { // Primary response - goes to hx-target @UserCard(user) // Out-of-band updates <span id="user-count" hx-swap-oob="true"> { fmt.Sprintf("%d users", totalUsers) } </span> <div id="notification" hx-swap-oob="innerHTML"> User created successfully! </div>}Tip
Out-of-band swaps are perfect for updating counters, notifications, navigation badges, or any UI that needs to stay in sync.
Loading Indicators
<!-- Show indicator during request --><button hx-get="/slow" hx-indicator="#spinner"> Load Data</button><span id="spinner" class="htmx-indicator">Loading...</span><!-- Disable button during request --><button hx-get="/data" hx-disabled-elt="this"> Load (disables while loading)</button><!-- CSS classes for loading state --><style> .htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } .htmx-request.htmx-indicator { display: inline; }</style>Event Handling
<!-- Reset form after success --><form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend" hx-on::after-request="this.reset()"> <input name="title" /> <button>Add</button></form><!-- Custom event handling --><div hx-get="/data" hx-on::before-request="console.log('Starting request')" hx-on::after-request="console.log('Request complete')" hx-on::response-error="alert('Error!')"> Load</div><!-- Listen for triggered events --><div hx-on:userCreated="this.classList.add('highlight')"> User list</div>Common Patterns
Search with Debounce
templ SearchForm() { <div class="search"> <input type="text" name="q" placeholder="Search..." hx-get="/search" hx-trigger="keyup changed delay:300ms" hx-target="#results" /> <div id="results"></div> </div>}Inline Editing
templ DisplayMode(item Item) { <div id={ "item-" + item.ID } hx-get={ "/items/" + item.ID + "/edit" } hx-trigger="click" hx-swap="outerHTML" class="editable" > { item.Name } </div>}templ EditMode(item Item) { <form id={ "item-" + item.ID } hx-put={ "/items/" + item.ID } hx-swap="outerHTML" > <input name="name" value={ item.Name } autofocus /> <button type="submit">Save</button> <button type="button" hx-get={ "/items/" + item.ID } hx-swap="outerHTML" > Cancel </button> </form>}Modal Dialog
templ ModalTrigger() { <button hx-get="/modal/confirm" hx-target="#modal-container" hx-swap="innerHTML" > Open Modal </button> <div id="modal-container"></div>}templ ConfirmModal(action string) { <div class="modal-backdrop" hx-on:click="this.remove()"> <div class="modal" hx-on:click="event.stopPropagation()"> <h2>Confirm Action</h2> <p>Are you sure you want to { action }?</p> <button hx-post={ "/actions/" + action } hx-target="#modal-container" hx-swap="innerHTML" > Confirm </button> <button hx-on:click="this.closest('.modal-backdrop').remove()"> Cancel </button> </div> </div>}Tabs
templ Tabs(activeTab string) { <div class="tabs"> <nav class="tab-nav"> <button class={ templ.KV("active", activeTab == "one") } hx-get="/tabs/one" hx-target="#tab-content" > Tab One </button> <button class={ templ.KV("active", activeTab == "two") } hx-get="/tabs/two" hx-target="#tab-content" > Tab Two </button> </nav> <div id="tab-content"> { children... } </div> </div>}Next Steps
- WebSockets - Real-time updates with HTMX
- Templ Templates - Template syntax reference
- HTMX Documentation - Full HTMX reference
