HT
htmx-patterns
HTMX + Django patterns for interactive UX without JavaScript complexity. Avoid JS conflicts using HTMX as single source of interactivity.
Install
mkdir -p .claude/skills/htmx-patterns && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15158" && unzip -o skill.zip -d .claude/skills/htmx-patterns && rm skill.zipInstalls to .claude/skills/htmx-patterns
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
HTMX + Django patterns for interactive UX without JavaScript complexity. Avoid JS conflicts using HTMX as single source of interactivity.137 chars · catalog descriptionno explicit “when” trigger
About this skill
HTMX + Django Patterns
Goal: Interactive transaction UI (filters, search, pagination) using HTMX + Django, avoiding JavaScript complexity and conflicts.
Quick Start
1. Install HTMX
HTML template:
<script src="https://unpkg.com/[email protected]"></script>
<!-- HTMX defaults for this project -->
<script>
htmx.config.defaultIndicatorStyle = "spinner";
htmx.config.timeout = 5000;
</script>
2. Core Principle: Replace JS
❌ Bad: onclick handlers + JavaScript
<button onclick="filterByCategory(5)">Salário</button>
<script>
function filterByCategory(id) {
fetch('/api/transactions/?category=' + id)
.then(r => r.text())
.then(html => {
document.getElementById('list').innerHTML = html;
});
}
</script>
✅ Good: HTMX only
<button hx-get="/transactions/filter/"
hx-vals='{"category": 5}'
hx-target="#transaction-list"
hx-swap="innerHTML">
Salário
</button>
HTMX Attributes Cheatsheet
<!-- Loading -->
hx-get="/endpoint/" <!-- GET request -->
hx-post="/endpoint/" <!-- POST request -->
hx-put="/endpoint/" <!-- PUT request -->
hx-delete="/endpoint/" <!-- DELETE request -->
<!-- Target & Swap -->
hx-target="#id" <!-- Replace this element -->
hx-target="closest .card" <!-- Find closest .card and replace -->
hx-swap="innerHTML" <!-- Replace content (default) -->
hx-swap="outerHTML" <!-- Replace element itself -->
hx-swap="beforeend" <!-- Append at end -->
hx-swap="afterbegin" <!-- Prepend at start -->
<!-- Triggering -->
hx-trigger="click" <!-- On click (default) -->
hx-trigger="change" <!-- On change (filters, selects) -->
hx-trigger="submit" <!-- On form submit -->
hx-trigger="every 2s" <!-- Polling every 2 seconds -->
hx-trigger="key[Enter]" <!-- Only on Enter key -->
<!-- Data & Values -->
hx-vals='{"key": "value"}' <!-- Add custom data -->
hx-include="[name=*]" <!-- Include form fields -->
hx-confirm="Sure?" <!-- Show confirmation -->
<!-- Indicators & Feedback -->
hx-indicator="#spinner" <!-- Show loading spinner -->
hx-disabled-elt="this" <!-- Disable button during request -->
hx-select="#target" <!-- Select only part of response -->
View Patterns (Django)
1. Filter View (CBV compatible)
# views.py
from django.views import View
from django.views.generic import ListView
from django.shortcuts import render
from .models import Transaction
class TransactionFilterView(View):
"""Handle HTMX filter requests"""
def get(self, request):
queryset = Transaction.objects.filter(user=request.user)
# Apply filters from query params
category = request.GET.get('category')
if category:
queryset = queryset.filter(category_id=category)
tipo = request.GET.get('tipo')
if tipo:
queryset = queryset.filter(tipo=tipo)
start_date = request.GET.get('start_date')
if start_date:
queryset = queryset.filter(data__gte=start_date)
# Paginate if needed
page = request.GET.get('page', 1)
from django.core.paginator import Paginator
paginator = Paginator(queryset, 20)
transactions = paginator.get_page(page)
context = {
'transactions': transactions,
'page_obj': transactions,
}
return render(request, 'transactions/list-items.html', context)
2. Live Search View
class TransactionSearchView(View):
"""Live search as user types"""
def get(self, request):
q = request.GET.get('q', '')
if len(q) < 2:
return render(request, 'transactions/search-results.html', {
'transactions': [],
})
# Search descricao or categoria
from django.db.models import Q
queryset = Transaction.objects.filter(
user=request.user
).filter(
Q(descricao__icontains=q) |
Q(categoria__nome__icontains=q)
)[:10]
context = {'transactions': queryset}
return render(request, 'transactions/search-results.html', context)
3. Dynamic Form Selects
class SubcategoryView(View):
"""Update subcategories when category changes"""
def get(self, request):
category_id = request.GET.get('category_id')
if not category_id:
subcategories = []
else:
from .models import Subcategory
subcategories = Subcategory.objects.filter(
category_id=category_id
)
context = {'subcategories': subcategories}
return render(request, 'transactions/subcategories-select.html', context)
Template Patterns
1. Filter Buttons
{% comment %} transactions/filters.html {% endcomment %}
<div class="filter-buttons">
<button hx-get="{% url 'transactions:filter' %}"
hx-vals='{"categoria": ""}'
hx-target="#transaction-list"
class="btn btn-sm">
Todas
</button>
{% for category in categories %}
<button hx-get="{% url 'transactions:filter' %}"
hx-vals='{"categoria": {{ category.id }} }'
hx-target="#transaction-list"
class="btn btn-sm">
{{ category.nome }}
</button>
{% endfor %}
</div>
2. Live Search Input
{% comment %} transactions/search.html {% endcomment %}
<input type="text"
placeholder="Buscar transações..."
hx-get="{% url 'transactions:search' %}"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
name="q"
class="form-input full-width">
<div id="search-results"></div>
{% comment %} Results render here from search-results.html {% endcomment %}
3. Dynamic Form (Dependent Selects)
{% comment %} transactions/form.html {% endcomment %}
<form hx-post="{% url 'transactions:create' %}"
hx-target="#form-errors"
hx-swap="innerHTML">
{% csrf_token %}
<!-- Category select -->
<select name="category"
hx-get="{% url 'transactions:subcategories' %}"
hx-target="#subcategory-group"
hx-trigger="change">
<option>Selecione...</option>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.nome }}</option>
{% endfor %}
</select>
<!-- Subcategories (updated by htmx) -->
<div id="subcategory-group">
<select name="subcategory">
<option>Selecione categoria primeiro</option>
</select>
</div>
<input type="number" name="valor" step="0.01" required>
<input type="date" name="data" required>
<button type="submit" hx-disabled-elt="this">
Criar
</button>
</form>
4. Pagination
{% comment %} transactions/pagination.html {% endcomment %}
<div id="transaction-list">
{% include "transactions/list-items.html" %}
</div>
{% comment %} Pagination links use HTMX {% endcomment %}
<div class="pagination">
{% if page_obj.has_previous %}
<a hx-get="{% url 'transactions:filter' %}"
hx-vals='{"page": {{ page_obj.previous_page_number }} }'
hx-target="#transaction-list"
class="btn">
← Anterior
</a>
{% endif %}
<span>Página {{ page_obj.number }} de {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a hx-get="{% url 'transactions:filter' %}"
hx-vals='{"page": {{ page_obj.next_page_number }} }'
hx-target="#transaction-list"
class="btn">
Próxima →
</a>
{% endif %}
</div>
Avoid JavaScript Conflicts
✅ Do This
<!-- HTMX triggers interaction -->
<button hx-post="/delete/"
hx-confirm="Deletar?"
hx-target="closest tr"
hx-swap="outerHTML swap:1s">
Deletar
</button>
❌ Never Do This
<!-- Don't mix JS + HTMX -->
<button onclick="deleteTransaction(5)"
hx-post="/delete/"> This conflicts!
Deletar
</button>
<!-- Don't use jQuery with HTMX -->
<button class="delete-btn">Deletar</button>
<script>
$('.delete-btn').click(function() { ... }) // Conflict!
</script>
Workaround: If External JS Exists
Use HTMX events to notify external code:
// external-listeners.js
document.addEventListener('htmx:afterSwap', function(detail) {
// React to HTMX swap if you must
console.log('Content swapped:', detail.detail);
});
But prefer HTMX only — avoid this complexity.
Integration with @django-patterns
Combine with CBV mixins:
# views.py using @django-patterns mixins
from django.contrib.auth.mixins import LoginRequiredMixin
class TransactionFilterView(LoginRequiredMixin, View):
"""HTMX filter + auth"""
def get(self, request):
queryset = Transaction.objects.filter(user=request.user)
# ... filtering logic ...
return render(request, 'transactions/list-items.html', context)
Use in URLs:
# urls.py
urlpatterns = [
path('transactions/filter/', TransactionFilterView.as_view(),
name='filter'),
path('transactions/search/', TransactionSearchView.as_view(),
name='search'),
]
Styling with @frontend-finance-design
Combine HTMX with Tailwind:
<!-- hx-indicator spinner -->
<div id="spinner" class="htmx-indicator hidden">
<div class="spinner-border animate-spin inline-block
---
*Content truncated.*