smithery.ai

templ-htmx

Build interactive hypermedia-driven applications with templ and HTMX. Use when creating dynamic UIs, real-time updates, AJAX interactions, mentions 'HTMX', 'dynamic content', or 'interactive templ app'.

First seen Mar 19, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,091 B
  • docs SUMMARY.md 220 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 3 installs

SKILL.md

Templ + HTMX Integration

Overview

HTMX enables modern, interactive web applications with minimal JavaScript. Combined with templ's type-safe components, you get fast, reliable hypermedia-driven UIs.

Key Benefits:

  • No JavaScript framework needed
  • Server-side rendering
  • Minimal client-side code
  • Progressive enhancement
  • Type-safe components

When to Use This Skill

Use when:

  • Building interactive UIs
  • Creating dynamic content
  • User mentions "HTMX", "dynamic updates", "real-time"
  • Implementing AJAX-like behavior without JS
  • Building SPAs without frameworks

Quick Start

1. Import and Mount HTMX

First, import the htmx package and mount it in your server:

import (
    "within.website/x/htmx"
)

func main() {
    mux := http.NewServeMux()

    // Mount HTMX static files at /.within.website/x/htmx/
    htmx.Mount(mux)

    // ... other routes
}

2. Add HTMX to Layout

package components

import "within.website/x/htmx"

templ Layout(title string) {
    <!DOCTYPE html>
    <html>
        <head>
            <title>{ title }</title>
            @htmx.Use()
        </head>
        <body>
            { children... }
        </body>
    </html>
}

The htmx.Use() component includes the core HTMX library. You can add extensions:

// Add SSE and path-params extensions
@htmx.Use("sse", "path-params")

Available extensions: event-header, path-params, remove-me, websocket.

3. Detect HTMX Requests

Use the htmx.Is() function to check if a request was made by HTMX:

import "within.website/x/htmx"

func handler(w http.ResponseWriter, r *http.Request) {
    if htmx.Is(r) {
        // Return partial HTML fragment for HTMX
        components.Partial().Render(r.Context(), w)
    } else {
        // Return full page for direct navigation
        components.FullPage().Render(r.Context(), w)
    }
}

4. Create Interactive Component

templ Counter(count int) {
    <div>
        <p>Count: { strconv.Itoa(count) }</p>
        <button
            hx-post="/counter/increment"
            hx-target="#counter"
            hx-swap="outerHTML"
        >
            Increment
        </button>
    </div>
}

5. Create Handler

func incrementHandler(w http.ResponseWriter, r *http.Request) {
    count := getCount() + 1
    saveCount(count)

    components.Counter(count).Render(r.Context(), w)
}

Core HTMX Attributes

hx-get / hx-post

Trigger HTTP requests:

templ SearchBox() {
    <input
        type="text"
        name="q"
        hx-get="/search"
        hx-trigger="keyup changed delay:500ms"
        hx-target="#results"
    />
    <div id="results"></div>
}

Handler:

func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    results := search(query)

    components.SearchResults(results).Render(r.Context(), w)
}

hx-target

Specify where to insert response:

templ LoadMore(page int) {
    <button
        hx-get={ "/posts?page=" + strconv.Itoa(page) }
        hx-target="#posts"
        hx-swap="beforeend"
    >
        Load More
    </button>
}

hx-swap

Control how content is swapped:

// innerHTML (default)
hx-swap="innerHTML"

// outerHTML - replace element itself
hx-swap="outerHTML"

// beforeend - append inside
hx-swap="beforeend"

// afterend - insert after
hx-swap="afterend"

hx-trigger

Control when requests fire:

// On click (default for buttons)
<button hx-get="/data">Click me</button>

// On change
<select hx-get="/filter" hx-trigger="change">

// On keyup with delay
<input hx-get="/search" hx-trigger="keyup changed delay:300ms">

// On page load
<div hx-get="/data" hx-trigger="load">

// Every 5 seconds
<div hx-get="/updates" hx-trigger="every 5s">

Common Patterns

Pattern 1: Live Search

Component:

templ SearchBox() {
    <div>
        <input
            type="text"
            name="q"
            placeholder="Search..."
            hx-get="/search"
            hx-trigger="keyup changed delay:500ms"
            hx-target="#search-results"
            hx-indicator="#spinner"
        />
        <span id="spinner" class="htmx-indicator">
            Searching...
        </span>
    </div>
    <div id="search-results"></div>
}

templ SearchResults(results []string) {
    <ul>
        for _, result := range results {
            <li>{ result }</li>
        }
    </ul>
}

Handler:

func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    results := performSearch(query)

    components.SearchResults(results).Render(r.Context(), w)
}

Pattern 2: Infinite Scroll

templ PostList(posts []Post, page int) {
    <div id="posts">
        for _, post := range posts {
            @PostCard(post)
        }
    </div>

    if len(posts) > 0 {
        <div
            hx-get={ "/posts?page=" + strconv.Itoa(page+1) }
            hx-trigger="revealed"
            hx-swap="outerHTML"
        >
            Loading more...
        </div>
    }
}

Pattern 3: Delete with Confirmation

templ DeleteButton(itemID string) {
    <button
        hx-delete={ "/items/" + itemID }
        hx-confirm="Are you sure?"
        hx-target="closest tr"
        hx-swap="outerHTML swap:1s"
    >
        Delete
    </button>
}

Handler:

func deleteHandler(w http.ResponseWriter, r *http.Request) {
    itemID := strings.TrimPrefix(r.URL.Path, "/items/")
    deleteItem(itemID)

    // Return empty to remove element
    w.WriteHeader(http.StatusOK)
}

Pattern 4: Inline Edit

templ EditableField(id string, value string) {
    <div id={ "field-" + id }>
        <span>{ value }</span>
        <button
            hx-get={ "/edit/" + id }
            hx-target={ "#field-" + id }
            hx-swap="outerHTML"
        >
            Edit
        </button>
    </div>
}

templ EditForm(id string, value string) {
    <form
        hx-post={ "/save/" + id }
        hx-target={ "#field-" + id }
        hx-swap="outerHTML"
    >
        <input type="text" name="value" value={ value } />
        <button type="submit">Save</button>
        <button
            hx-get={ "/cancel/" + id }
            hx-target={ "#field-" + id }
        >
            Cancel
        </button>
    </form>
}

Pattern 5: Form Validation

templ SignupForm() {
    <form hx-post="/signup" hx-target="#form-errors">
        <div id="form-errors"></div>

        <input
            type="email"
            name="email"
            hx-post="/validate/email"
            hx-trigger="blur"
            hx-target="#email-error"
        />
        <div id="email-error"></div>

        <input type="password" name="password" />

        <button type="submit">Sign Up</button>
    </form>
}

templ ValidationError(message string) {
    <span class="error">{ message }</span>
}

Pattern 6: Polling / Real-time Updates

templ LiveStats() {
    <div
        hx-get="/stats"
        hx-trigger="load, every 5s"
        hx-swap="innerHTML"
    >
        Loading stats...
    </div>
}

templ StatsDisplay(stats Stats) {
    <div>
        <p>Users online: { strconv.Itoa(stats.UsersOnline) }</p>
        <p>Active sessions: { strconv.Itoa(stats.Sessions) }</p>
    </div>
}

Advanced Patterns

Out-of-Band Updates (OOB)

Update multiple parts of page:

templ CartButton(count int) {
    <button id="cart-btn">
        Cart ({ strconv.Itoa(count) })
    </button>
}

templ AddToCartResponse(item Item) {
    // Main response
    <div class="notification">
        Added { item.Name } to cart!
    </div>

    // Update cart button (different part of page)
    <div id="cart-btn" hx-swap-oob="true">
        @CartButton(getCartCount())
    </div>
}

Progressive Enhancement

templ Form() {
    <form
        action="/submit"
        method="POST"
        hx-post="/submit"
        hx-target="#result"
    >
        <input type="text" name="data" />
        <button type="submit">Submit</button>
    </form>
    <div id="result"></div>
}

Works without JavaScript, enhanced with HTMX.

Loading States

templ DataTable() {
    <div
        hx-get="/data"
        hx-trigger="load"
        hx-indicator="#loading"
    >
        <div id="loading" class="htmx-indicator">
            Loading data...
        </div>
    </div>
}

CSS:

.htmx-indicator {
  display: none;
}

.htmx-request .htmx-indicator {
  display: inline;
}

.htmx-request.htmx-indicator {
  display: inline;
}

Response Headers

HX-Trigger

Trigger client-side events:

func handler(w http.ResponseWriter, r *http.Request) {
    // Do work...

    // Trigger custom event
    w.Header().Set("HX-Trigger", "itemCreated")

    components.Success().Render(r.Context(), w)
}

Client side:

document.body.addEventListener("itemCreated", function (evt) {
  console.log("Item created!");
});

HX-Redirect

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("HX-Redirect", "/dashboard")
    w.WriteHeader(http.StatusOK)
}

HX-Refresh

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("HX-Refresh", "true")
    w.WriteHeader(http.StatusOK)
}

Special Status Code

Stop polling with status code 286:

import "within.website/x/htmx"

func pollHandler(w http.ResponseWriter, r *http.Request) {
    if shouldStopPolling() {
        w.WriteHeader(htmx.StatusStopPolling)
        return
    }
    // ... return normal content
}

Request Headers

Access HTMX request headers:

import "within.website/x/htmx"

func handler(w http.ResponseWriter, r *http.Request) {
    // Check if this is an HTMX request
    if htmx.Is(r) {
        // Get user's response to hx-prompt
        promptResponse := r.Header.Get(htmx.HeaderPrompt)
    }
}

Best Practices

  1. Keep handlers focused - Return only the HTML fragment needed
  2. Use semantic HTML - Works without JS
  3. Handle errors gracefully - Return error components
  4. Optimize responses - Send minimal HTML
  5. Use OOB for multi-updates - Update multiple page sections
  6. Progressive enhancement - Always provide fallback

Full Example: Todo App

// components/todo.templ
package components

type Todo struct {
    ID        string
    Text      string
    Completed bool
}

templ TodoApp(todos []Todo) {
    @Layout("Todo App") {
        <div>
            <h1>My Todos</h1>

            @TodoForm()
            @TodoList(todos)
        </div>
    }
}

templ TodoForm() {
    <form
        hx-post="/todos"
        hx-target="#todo-list"
        hx-swap="beforeend"
        hx-on::after-request="this.reset()"
    >
        <input
            type="text"
            name="text"
            placeholder="New todo..."
            required
        />
        <button type="submit">Add</button>
    </form>
}

templ TodoList(todos []Todo) {
    <ul id="todo-list">
        for _, todo := range todos {
            @TodoItem(todo)
        }
    </ul>
}

templ TodoItem(todo Todo) {
    <li id={ "todo-" + todo.ID }>
        <input
            type="checkbox"
            checked?={ todo.Completed }
            hx-post={ "/todos/" + todo.ID + "/toggle" }
            hx-target={ "#todo-" + todo.ID }
            hx-swap="outerHTML"
        />
        <span class={ templ.KV("completed", todo.Completed) }>
            { todo.Text }
        </span>
        <button
            hx-delete={ "/todos/" + todo.ID }
            hx-target={ "#todo-" + todo.ID }
            hx-swap="outerHTML swap:500ms"
        >
            Delete
        </button>
    </li>
}

Handlers:

func todosHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
        todos := getAllTodos()
        components.TodoApp(todos).Render(r.Context(), w)

    case "POST":
        r.ParseForm()
        todo := createTodo(r.FormValue("text"))
        components.TodoItem(todo).Render(r.Context(), w)
    }
}

func todoToggleHandler(w http.ResponseWriter, r *http.Request) {
    id := extractID(r.URL.Path)
    todo := toggleTodo(id)
    components.TodoItem(todo).Render(r.Context(), w)
}

func todoDeleteHandler(w http.ResponseWriter, r *http.Request) {
    id := extractID(r.URL.Path)
    deleteTodo(id)
    w.WriteHeader(http.StatusOK) // Empty response removes element
}

Resources

Next Steps

  • Style components → Use templ-css skill
  • Deploy → Use templ-deployment skill
  • Test → Use templ-testing skill