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:

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=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 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)

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: 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 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-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 ($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 (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

See Also