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 htmx

  • Mode: template (or htmx) with HTMXPlugin

  • Entry: resources/main.js (minimal JS)

  • Dev: litestar run --reload (or litestar 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:

Imports for HTMX application#

VitePlugin and app configuration#
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 rendering

  • HTMXPlugin() adds HTMX-specific request/response handling

  • Templates use .html.j2 extension (Jinja2)

Base Template#

The base template sets up Vite HMR and the Litestar HTMX extension:

templates/base.html.j2#
<!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 assets

  • hx-ext="litestar" - Enables the Litestar HTMX extension for JSON templating

  • csrf_token - CSRF protection for forms and HTMX requests

Frontend Entry Point#

The Litestar HTMX extension must be registered explicitly from your Vite entry file:

resources/main.js#
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 = htmx exposes the HTMX runtime to the extension

  • registerHtmxExtension() wires in CSRF header injection and JSON templating support

  • htmx.process(document.body) activates declarative HTMX behavior after the bundle loads

JSON event streams#

For a JSON WebSocket or SSE endpoint, register <litestar-stream> instead of adding htmx-ext-ws or htmx-ext-sse:

import { defineStreamElement } from "litestar-vite-plugin/helpers"

defineStreamElement()

The element binds the connection to DOM lifetime and can render frames through the existing swap="json" template engine. It replaces the htmx stream extensions rather than layering another reconnect policy on top of them. See WebSocket and SSE event streams.

HTMX Fragments#

Return partial HTML for HTMX swaps using HTMXTemplate:

Fragment endpoint with HTMXTemplate#
    @get("/fragments/book/{book_id:int}")
    async def book_fragment(self, book_id: FromPath[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 element

  • re_swap - Override the swap method

  • push_url - Control browser history

Partial Template#

Fragment templates are simple Jinja2 partials:

templates/partials/book_card.html.j2#
<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 ($data is the response)

  • ls-key="item.id" - Unique key for efficient updates

  • ls-if="condition" - Conditional rendering

  • ls-else - Else branch for conditionals

  • ${expression} - Interpolate values with the fixed expression grammar

This enables hybrid rendering: server-side HTML for initial load, client-side templating for dynamic updates from JSON APIs.

The expression grammar supports property access, arithmetic, comparisons, logical and ternary operators, string template and object literals, method calls on values, calls to context functions, and the JSON and Math namespaces. Assignment, new, function and arrow expressions, computed indexing such as obj[key], array literals, and arbitrary browser globals are intentionally unsupported.

In @event handlers, $event is a sanitized plain object rather than the raw DOM event. It exposes type, target.value, target.name, target.checked, target.id, target.type, preventDefault(), and stopPropagation(). Host-object paths such as $event.view and $event.target.ownerDocument are intentionally unavailable.

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

See Also#