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 Update

The HTMX/irgo approach:

User Click → HTMX Request → Go Handler → HTML Response → DOM Swap

Your 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