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'.
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:
Use when:
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
}
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.
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)
}
}
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>
}
func incrementHandler(w http.ResponseWriter, r *http.Request) {
count := getCount() + 1
saveCount(count)
components.Counter(count).Render(r.Context(), w)
}
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)
}
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>
}
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"
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">
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)
}
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>
}
}
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)
}
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>
}
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>
}
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>
}
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>
}
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.
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;
}
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!");
});
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("HX-Redirect", "/dashboard")
w.WriteHeader(http.StatusOK)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("HX-Refresh", "true")
w.WriteHeader(http.StatusOK)
}
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
}
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)
}
}
// 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
}
templ-css skilltempl-deployment skilltempl-testing skill下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer