HTMX¶
HTMX integration for hypermedia-driven applications with minimal JavaScript. Litestar-Vite provides seamless integration with the litestar-htmx extension.
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)
└── style.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=5061),
)
)
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)
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."""
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-gradient-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