HTMX Template Specification¶
Version: 1.0.0 | Status: Active | MCP Resource: dazzle://docs/htmx-templates
Purpose¶
This specification enables LLM coding agents to produce rich, powerful HTMX interfaces with minimal context. Design principles:
- Server Authority — Server renders HTML, client displays it
- Visible Behavior — All interactions are
hx-*attributes, not hidden JavaScript - Predictable Structure — Same patterns across all entities
- Composable Fragments — Complex UIs from simple, reusable parts
Quick Reference¶
HTMX Attributes¶
| Pattern | Attributes | Use Case |
|---|---|---|
| Search | hx-get hx-trigger="keyup changed delay:400ms" hx-target |
Debounced input |
| Navigate | hx-get hx-push-url="true" hx-target="body" |
Row click |
| Delete | hx-delete hx-confirm hx-target="closest tr" hx-swap="outerHTML" |
Inline delete |
| Submit | hx-post hx-target hx-swap="outerHTML" |
Form submission |
| Autofill | hx-swap-oob="outerHTML" |
Multi-field update |
Target Naming¶
| Type | Pattern | Example |
|---|---|---|
| Table body | #{entity}-table-body |
#contact-table-body |
| Pagination | #{entity}-pagination |
#contact-pagination |
| Search results | #{field}-results |
#company-search-results |
| Form field | #field-{name} |
#field-company_number |
| Spinner | #{context}-spinner |
#search-spinner |
Three-Layer Architecture¶
┌─────────────────────────────────────────────────────────┐
│ COMPONENTS │
│ Full page content: list_view, detail_view, form │
│ One component per surface mode (~100-150 lines) │
└────────────────────────┬────────────────────────────────┘
│ include
┌────────────────────────▼────────────────────────────────┐
│ FRAGMENTS │
│ HTMX-swappable partials: table_rows, search_select │
│ Addressable by DOM id, communicate via events (~20-50) │
└────────────────────────┬────────────────────────────────┘
│ call
┌────────────────────────▼────────────────────────────────┐
│ MACROS │
│ Rendering helpers: form_field, status_badge │
│ Pure functions: params in, HTML out (~10-30 lines) │
└─────────────────────────────────────────────────────────┘
Layer Rules¶
- Components define layout, include fragments, set up swap targets
- Fragments are swapped by HTMX, emit/listen to events, have formal contracts
- Macros are pure rendering, never called by HTMX directly
Fragment Contracts¶
Each fragment has a formal interface. Discover via MCP:
Contract Structure¶
fragment: search_select
template: fragments/search_select.html
params:
required:
- field.name
- field.label
- field.source.endpoint
optional:
- field.placeholder
- field.source.debounce_ms # default: 400
- field.source.min_chars # default: 3
emits:
- itemSelected # Fired on selection
listens: []
swap_targets:
- "#{{ field.name }}-results"
oob_targets: # Fields populated on selection
- "#field-{{ autofill_target }}"
Standard Events¶
| Event | Meaning | Emitted By |
|---|---|---|
itemSelected |
User selected from list/dropdown | search_select, table row |
formSaved |
Form successfully submitted | form components |
rowDeleted |
Table row removed | delete button |
searchCleared |
Search input cleared | search_input |
Cognition Strategies¶
Before Modifying Templates¶
- Check fragment contracts:
mcp__dazzle__dsl(operation="list_fragments") - Inspect surface spec:
mcp__dazzle__dsl(operation="inspect_surface", name="...") - Understand entity:
mcp__dazzle__dsl(operation="inspect_entity", name="...")
Modification Patterns¶
| Task | Where to Edit | Lines Changed |
|---|---|---|
| Add table column | Component: add <th> and <td> |
~4 |
| Add search filter | Fragment: add input with hx-get |
~8 |
| Add delete button | Fragment: add button with hx-delete |
~5 |
| Add form field | Macro call: add {{ form_field(field) }} |
~1 |
| Add autofill | Server: add OOB fragment to response | ~10 |
Context Window Efficiency¶
Templates fit in small context windows:
| Component | Lines | Rationale |
|---|---|---|
| List view | 100-150 | Table + search + pagination targets |
| Form | 80-120 | Field iteration + validation display |
| Fragment | 20-50 | Single responsibility |
| Macro | 10-30 | Pure rendering |
Full feature: component + 2-3 fragments ≈ 300 lines
OOB Swap Pattern¶
For updating multiple DOM elements from one response:
<!-- Server response -->
<!-- Primary swap: goes to hx-target -->
<div id="company-selected" class="alert alert-success">
Selected: Acme Ltd
</div>
<!-- OOB swaps: go to their own element ids -->
<input id="field-company_number" value="12345678"
hx-swap-oob="outerHTML" readonly />
<input id="field-company_status" value="active"
hx-swap-oob="outerHTML" readonly />
Server renders OOB fields using the same macro as the original form for consistency.
Event Communication¶
Fragments communicate through events, not shared state:
<!-- Fragment A emits -->
<button hx-get="..."
hx-on::after-request="htmx.trigger(this, 'itemSelected')">
<!-- Fragment B listens -->
<div hx-get="..."
hx-trigger="itemSelected from:closest form">
Communication Channels¶
- HTMX Events —
hx-trigger="eventName from:selector" - OOB Swaps — Server response includes multiple fragments
- URL Parameters — For cross-page state (pagination, filters)
DSL Integration¶
Surface → Template Mapping¶
surface contact_list → components/list_view.html
mode: list └── fragments/table_rows.html
section main: └── fragments/table_pagination.html
field first_name
surface contact_edit → components/form_edit.html
mode: edit └── fragments/form_errors.html
section info: └── macros/form_field.html
field company_name:
source: companieshouse └── fragments/search_select.html
Field Type → Rendering¶
| DSL Type | Renders As |
|---|---|
string, text |
<input type="text"> via form_field macro |
email |
<input type="email"> via form_field macro |
date |
<input type="date"> via form_field macro |
enum |
<select> via form_field macro |
boolean |
Checkbox via form_field macro |
Field with source: |
search_select fragment |
CDN Dependencies¶
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
Vanilla JS Scope (Limited)¶
Vanilla JS manages UI state only: dropdown open/close, modal visibility, accordion state.
JS does NOT manage: form data, API responses, selection state. HTMX handles all server communication.
Non-Goals¶
This specification does not cover:
- Virtual DOM or client-side diffing
- Client-side state management
- Build toolchains (webpack, vite)
- TypeScript (server-rendered HTML needs no client types)
- Real-time collaboration / WebSockets
- Offline-first / service workers
Template Overrides¶
Projects can override framework templates by placing files with the same
name in their templates/ directory. The Jinja2 ChoiceLoader searches
the project directory first, then the framework directory.
The dz:// prefix¶
A project override that needs to extend the framework template it
replaces must use the dz:// prefix to avoid circular inheritance:
{# dazzle:override base.html #}
{# dazzle:blocks scripts_extra #}
{% extends "dz://base.html" %}
{% block scripts_extra %}
{{ super() }}
<script src="/static/js/my-custom.js"></script>
{% endblock %}
Without the dz:// prefix, {% extends "base.html" %} resolves to the
project's own file (ChoiceLoader priority), causing infinite recursion.
Declaration headers¶
The {# dazzle:override <target> #} and {# dazzle:blocks <block-list> #}
headers are optional but recommended. They register the override in
.dazzle/overrides.json so the framework can check compatibility across
upgrades.
Available blocks in base.html¶
| Block | Purpose |
|---|---|
head_extra |
Additional <head> content (meta tags, stylesheets) |
body |
Full page body (rarely overridden directly) |
scripts_extra |
Additional <script> tags before </body> |
Layout templates (layouts/app_shell.html, etc.) expose additional blocks
for sidebar, navbar, and content areas.
CLI commands¶
dazzle overrides scan # Scan project templates/ for override declarations
dazzle overrides check # Verify overrides are compatible with current framework
dazzle overrides list # List registered overrides from .dazzle/overrides.json
How the loader works¶
Project templates/ Framework templates/
base.html ──────────┐
├── ChoiceLoader (project wins)
dz://base.html ─────┤── PrefixLoader (always framework)
│
└── Framework base.html
template_renderer.py configures this via ChoiceLoader + PrefixLoader
with "dz" as the prefix and "://" as the delimiter.