HTMX¶
HTMX integration for hypermedia-driven applications with minimal JavaScript. Litestar-Vite provides seamless integration with the litestar-htmx extension.
At a Glance¶
Template:
litestar assets init --template htmxMode:
template(orhtmx) withHTMXPluginEntry:
resources/main.js(minimal JS)Dev:
litestar run --reload(orlitestar assets serve+litestar run)
Quick Start¶
litestar assets init --template htmx
Project Structure¶
my-app/
├── app.py # Litestar backend with HTMX
├── package.json
├── vite.config.ts
├── templates/
│ ├── base.html.j2 # Base template with Vite + HTMX
│ ├── index.html.j2 # Main page
│ └── partials/
│ └── book_card.html.j2 # Reusable fragment
└── resources/
├── main.js # Entry (minimal)
└── styles.css
Backend Setup¶
HTMX applications use mode="template" with the HTMXPlugin from litestar-htmx:
vite = VitePlugin(
config=ViteConfig(
mode="template",
dev_mode=DEV_MODE,
paths=PathConfig(root=here, resource_dir="resources"),
types=TypeGenConfig(output=Path("resources/generated"), generate_sdk=False),
runtime=RuntimeConfig(port=5050),
)
)
templates = TemplateConfig(directory=here / "templates", engine=JinjaTemplateEngine)
app = Litestar(route_handlers=[LibraryController], plugins=[vite, HTMXPlugin()], template_config=templates, debug=True)
Key points:
mode="template"enables Jinja2 template renderingHTMXPlugin()adds HTMX-specific request/response handlingTemplates use
.html.j2extension (Jinja2)
Base Template¶
The base template sets up Vite HMR and the Litestar HTMX extension:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token | default('') }}" />
<title>{% block title %}{{ project_name }}{% endblock %}</title>
{{ vite_hmr() }}
{{ vite('resources/main.js') }}
</head>
<body hx-ext="litestar">
{% block content %}{% endblock %}
</body>
</html>
Key features:
{{ vite_hmr() }}- Enables hot module replacement in development{{ vite('resources/main.js') }}- Loads bundled assetshx-ext="litestar"- Enables the Litestar HTMX extension for JSON templatingcsrf_token- CSRF protection for forms and HTMX requests
Frontend Entry Point¶
The Litestar HTMX extension must be registered explicitly from your Vite entry file:
import htmx from "htmx.org"
import "./styles.css"
// Register the Litestar HTMX extension so JSON templating and CSRF headers work automatically
import { registerHtmxExtension } from "litestar-vite-plugin/helpers"
// Import type-safe route helper for use in JSON templates
import { route } from "./generated/routes"
window.htmx = htmx
// Make route() available globally for JSON template expressions
// Templates can now use: route("book_detail", { book_id: book.id })
window.route = route
registerHtmxExtension()
htmx.process(document.body)
Key points:
window.htmx = htmxexposes the HTMX runtime to the extensionregisterHtmxExtension()wires in CSRF header injection and JSON templating supporthtmx.process(document.body)activates declarative HTMX behavior after the bundle loads
HTMX Fragments¶
Return partial HTML for HTMX swaps using HTMXTemplate:
@get("/fragments/book/{book_id:int}")
async def book_fragment(self, book_id: int) -> Template:
"""Return a book card fragment for HTMX swaps.
Returns:
The result.
"""
book = _get_book(book_id)
return HTMXTemplate(
template_name="partials/book_card.html.j2",
context={"book": book},
re_target="#book-detail",
re_swap="innerHTML",
push_url=False,
)
The HTMXTemplate response allows:
re_target- Override the target elementre_swap- Override the swap methodpush_url- Control browser history
Partial Template¶
Fragment templates are simple Jinja2 partials:
<article class="space-y-2 rounded-xl border border-slate-200 bg-linear-to-b from-white to-slate-50 p-4 shadow-sm">
<h3 class="text-lg font-semibold text-[#202235]">{{ book.title }}</h3>
<p class="mt-1 text-slate-600">{{ book.author }} • {{ book.year }}</p>
<p class="mt-1 text-sm text-[#202235]">{{ book.tags | join(' · ') }}</p>
<button
class="inline-flex items-center gap-2 rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white shadow hover:bg-slate-800"
hx-get="/fragments/book/{{ book.id }}"
hx-target="#book-detail"
hx-swap="innerHTML"
>
Refresh details
</button>
</article>
JSON Templating (Litestar Extension)¶
The hx-ext="litestar" extension enables client-side JSON templating
using hx-swap="json" with template directives:
<!-- Fetch JSON and render client-side -->
<button hx-get="/api/books" hx-target="#books" hx-swap="json">
Load Books
</button>
<div id="books">
<!-- ls-for iterates over JSON array -->
<template ls-for="book in $data" ls-key="book.id">
<article>
<h3>${book.title}</h3>
<p>${book.author} - ${book.year}</p>
</article>
</template>
</div>
Template directives:
ls-for="item in $data"- Iterate over JSON response ($datais the response)ls-key="item.id"- Unique key for efficient updatesls-if="condition"- Conditional renderingls-else- Else branch for conditionals${expression}- Interpolate values (JavaScript template literal syntax)
This enables hybrid rendering: server-side HTML for initial load, client-side templating for dynamic updates from JSON APIs.
HTMX Patterns¶
Inline Editing:
<div hx-get="/edit/{{ item.id }}"
hx-trigger="click"
hx-swap="outerHTML">
{{ item.name }}
</div>
Form Submission:
<form hx-post="/items"
hx-target="#items"
hx-swap="beforeend">
<input name="name" required>
<button type="submit">Add</button>
</form>
Delete with Confirmation:
<button hx-delete="/items/{{ item.id }}"
hx-confirm="Delete this item?"
hx-target="closest li"
hx-swap="outerHTML">
Delete
</button>
Why HTMX?¶
Minimal JavaScript: Most interactivity via HTML attributes
Server-rendered: Full HTML responses, great for SEO
Progressive enhancement: Works without JS (degrades gracefully)
Simple mental model: Request → HTML response → DOM update
JSON templating: Client-side rendering when needed via Litestar extension