brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
htmxUse this skill when the user says 'htmx', 'htmx setup', 'htmx ajax', 'htmx hx-get', 'htmx hx-post', 'htmx HATEOAS', 'htmx hypermedia', 'htmx server-driven', or when building hypermedia-driven applications with htmx. This skill enforces: server-driven AJAX via HTML attributes, HATEOAS principles, minimal JavaScript, partial page replacements, hypermedia as t…
Independent GitHub snapshot · Automated classification · Original documentation governs
Build hypermedia-driven web applications where the server sends HTML fragments in response to AJAX requests triggered by HTML attributes — no JavaScript required.
Exact user phrases: "htmx setup", "htmx project", "htmx hx-get", "htmx hx-post", "htmx HATEOAS", "htmx hypermedia", "htmx server", "htmx ajax", "hypermedia app".
Before activating, verify:
No file output. Produces HTML snippets with htmx attributes as text.
HTML with htmx attributes:
<button hx-get="/api/data" hx-target="#result" hx-swap="innerHTML">
Load Data
</button>
<div id="result"></div>
No preamble. No postamble. No explanations. Compress output — why use many token when few do trick.
~4096 tokens.
| Approach | Trade-off | When to Use |
|---|---|---|
| hx-get fragment swap | Simple data fetch, GET semantics | Read operations, lazy load |
| hx-post form with validation | Form submission, 422 errors | Mutations with validation |
| hx-boost on links/forms | Full-page SPA-like navigation | Enhancing existing HTML |
| hx-trigger="every 10s" | Polling for updates | Real-time-ish dashboards |
| hx-trigger="revealed" | Lazy load below fold | Performance optimization |
| hx-trigger="intersect" | Viewport-based loading | Infinite scroll |
| Server-Sent Events (hx-sse) | Server push | Real-time notifications |
| WebSockets (hx-ws) | Bidirectional stream | Chat, collaborative editing |
What triggers the AJAX request?
Button/link click -> hx-trigger="click" (default for buttons, links, forms)
Page load -> hx-trigger="load"
Element scrolled into view -> hx-trigger="revealed"
Input change -> hx-trigger="change, keyup delay:300ms" (search-as-you-type)
Time interval -> hx-trigger="every 5s" (polling)
Intersection observer -> hx-trigger="intersect threshold:0.5"
Custom JS event -> hx-trigger="custom-event from:body"
Focus loss -> hx-trigger="focusOut delay:200ms"
What should happen with the response?
Replace target's children -> hx-swap="innerHTML" (default)
Replace the whole target -> hx-swap="outerHTML"
Insert before the target -> hx-swap="beforebegin"
Insert after the target -> hx-swap="afterend"
Append inside target -> hx-swap="beforeend"
Prepend inside target -> hx-swap="afterbegin"
Execute response JS only -> hx-swap="none"
Remove the target -> hx-swap="delete"
How much of the page does this interaction affect?
Small widget update -> Return fragment, swap innerHTML on container
Form submission with errors -> Return updated form HTML with 422 status
Section replacement (tab) -> Return section HTML, swap outerHTML on section
Full page navigation -> Use hx-boost on links for seamless navigation
Modal/dialog -> Return dialog HTML, swap innerHTML on modal container
Infinite scroll -> Return next page items, swap beforeend on list
<button hx-get="/api/refresh" hx-target="#data" hx-indicator="#spinner">
Refresh
<img id="spinner" class="htmx-indicator" src="/spinner.gif" />
</button>
<form hx-post="/contacts" hx-target="#form-area" hx-swap="outerHTML">
<input type="text" name="email" hx-get="/contacts/check-email"
hx-trigger="change" hx-target="#email-error">
<div id="email-error"></div>
<button type="submit">Submit</button>
</form>
Server returns updated form HTML with class "error" on invalid fields and HTTP 422 status.
<div hx-get="/posts?page=2" hx-trigger="revealed" hx-swap="beforeend" hx-target="#posts">
</div>
<div id="posts">
<!-- Existing posts rendered by server -->
</div>
<div hx-target="#tab-content" hx-swap="innerHTML">
<a hx-get="/tabs/info">Info</a>
<a hx-get="/tabs/settings">Settings</a>
</div>
<div id="tab-content">
<!-- Server-rendered tab content -->
</div>
<div hx-get="/contacts/1/edit" hx-trigger="dblclick" hx-target="this" hx-swap="outerHTML">
<span>{{ contact.name }}</span>
</div>
Server returns <form> with input filled in and hx-put to cancel/submit.
State lives on the server. Client is a stateless HTML viewer. The server:
<div hx-get="/products?page=2" hx-push-url="true" hx-target="#products">
Next Page
</div>
For client-only UI state (modals, toggles, theme), pair htmx with Alpine.js:
<div x-data="{ modalOpen: false }">
<button hx-get="/api/data" hx-target="#result" @click.prevent>Load</button>
<div x-show="modalOpen">Modal content</div>
</div>
delay:300ms to avoid excess requests.npm install htmx.org, then import 'htmx.org'.hx-ext) each add 2-5KB — only load what you need.Since htmx is primarily server-driven, testing focuses on the backend:
# pytest example for Django + htmx
def test_contact_list_htmx(client):
response = client.get('/contacts', HTTP_HX_REQUEST='true')
assert response.status_code == 200
assert 'partials/_contact_list.html' in response.templates
assert 'hx-get' in response.content.decode()
test('infinite scroll loads more items', async ({ page }) => {
await page.goto('/posts')
const initialCount = await page.locator('.post').count()
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForResponse(resp => resp.url().includes('/posts?page=2'))
const newCount = await page.locator('.post').count()
expect(newCount).toBeGreaterThan(initialCount)
})
| jQuery Pattern | htmx Equivalent |
|---|---|
$.get('/url', fn) | <button hx-get="/url" hx-target="#result"> |
$.post('/url', data, fn) | <form hx-post="/url" hx-target="#result"> |
$.ajax({error: fn}) | Server returns error HTML, CSS on .htmx-request errors |
| Manual loading spinner | hx-indicator="#spinner" |
$(el).html(html) | hx-swap="innerHTML" |
$(el).replaceWith(html) | hx-swap="outerHTML" |
Migration strategy: Wrap each AJAX interaction with htmx attributes. Remove jQuery AJAX calls as you go.
Fundamental architecture change (client to server rendering). Best for content-heavy apps:
| Aspect | htmx | React | Alpine.js |
|---|---|---|---|
| Rendering | Server HTML | Client VDOM | Client DOM |
| State location | Server | Client state tree | DOM + Alpine.store |
| API responses | HTML fragments | JSON | JSON |
| Learning curve | Hours | Weeks | Days |
| Bundle size | ~14KB | ~120KB (min) | ~10KB |
| SEO | Full HTML | Needs SSR | HTML baseline |
def contact_list(request):
if request.htmx:
return render(request, 'contacts/_list.html', {'contacts': Contact.objects.all()})
return render(request, 'contacts/index.html', {'contacts': Contact.objects.all()})
Route::get('/contacts', function () {
if (request()->header('HX-Request')) {
return view('contacts/_list', ['contacts' => Contact::all()]);
}
return view('contacts/index', ['contacts' => Contact::all()]);
});
app.get('/contacts', (req, res) => {
if (req.headers['hx-request']) {
return res.render('contacts/_list', { contacts })
}
res.render('contacts/index', { contacts })
})
<input type="search" name="q" hx-post="/search" hx-trigger="input changed delay:500ms, search" hx-target="#search-results" hx-indicator="#spinner">
<div hx-get="/graphs/revenue" hx-trigger="load"><img class="htmx-indicator" src="/spinner.gif"></div>
<button hx-delete="/contacts/1" hx-confirm="Delete contact?" hx-target="closest tr">Delete</button>
hyperscript — companion language (optional)alpinejs — complementary for client interactivitydjango-htmx — Django middlewareflask-htmx — Flask extensionlaravel-htmx — Laravel packagego-htmx — Go middlewarespring-htmx — Spring Boot integrationNo artifact produced. Next skill: htmx-hyperscript (if client-side logic needed) or backend-htmx-integration. Carry forward: hx-trigger/hx-target/hx-swap pattern, HTML-fragment responses, HATEOAS.
<!-- Server-rendered component with HTMX -->
<div hx-target="this" hx-swap="outerHTML">
<button hx-get="/api/component/counter"
hx-trigger="click"
hx-vals='{"action": "increment"}'>
Count: {{ count }}
</button>
</div>
<!-- Inline editing pattern -->
<div hx-target="this" hx-swap="outerHTML">
<span hx-get="/api/component/edit/{{ id }}"
hx-trigger="click"
class="editable">
{{ value }}
</span>
</div>
<!-- Lazy loading pattern -->
<div hx-get="/api/component/heavy-content"
hx-trigger="load"
hx-swap="innerHTML">
<div class="spinner">Loading...</div>
</div>
<!-- Infinite scroll -->
<div hx-get="/api/items?page=2"
hx-trigger="revealed"
hx-swap="afterend"
hx-target="this">
</div>
<!-- Form validation -->
<form hx-post="/api/users"
hx-target="#form-errors"
hx-swap="innerHTML">
<input type="text" name="email"
hx-post="/api/validate/email"
hx-trigger="change"
hx-target="next .error">
<div class="error"></div>
<button type="submit">Submit</button>
</form>
<div id="form-errors"></div>
from flask import Blueprint, request, render_template, jsonify
htmx = Blueprint('htmx', __name__)
@htmx.route('/api/component/counter')
def counter_component():
action = request.form.get('action', 'increment')
current = int(request.args.get('count', 0))
if action == 'increment':
current += 1
elif action == 'decrement':
current -= 1
return render_template('components/_counter.html', count=current)
@htmx.route('/api/component/edit/<id>')
def edit_component(id):
value = get_value(id)
return render_template('components/_edit_form.html', id=id, value=value)
@htmx.route('/api/validate/email', methods=['POST'])
def validate_email():
email = request.form.get('email', '')
if '@' not in email:
return '<span class="error">Invalid email</span>'
return '<span class="success">Valid email</span>'
What's the interactivity requirement?
├── Simple CRUD, forms, navigation
│ └── HTMX + server-side rendering
│ ├── Less JavaScript, simpler architecture
│ ├── Server handles rendering
│ └── Faster initial load
│
├── Complex real-time UIs (drag-drop, drawing)
│ └── SPA (React, Vue, Svelte)
│ ├── Rich client interactivity
│ ├── Complex state management
│ └── More JavaScript overhead
│
├── Mixed (mostly simple, some complex)
│ └── HTMX for simple parts + SPA island for complex
│ └── Best of both worlds
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Returning JSON from HTMX endpoints | Client has to parse and render | Return HTML fragments directly |
| Using htmx for everything | Not appropriate for rich UIs | Use SPA for complex interactive parts |
| No loading indicators | Users don't know if request is working | Always use hx-indicator |
| Wrong swap strategy | Unexpected UI behavior | Choose correct hx-swap for each use case |
| Server endpoints not idempotent | Duplicate requests cause issues | GET should be safe, POST can repeat |
hx-swap="morphdom" for fine-grained DOM diffing. Only changes the parts of the element that actually changed. Reduces layout thrash.