Islands Architecture & HTMX Fragments¶
When building server-driven applications in mode="template" (or mode="htmx"), you often want 90% of a page rendered via fast Jinja2 HTML templates or HTMX partial swaps while hydrating specific interactive components (charts, command palettes, rich editors) in React, Vue, or Svelte.
Litestar Vite provides two complementary primitives powered by FragmentEngine:
vite_fragment()+<litestar-island>: Pre-renders a UI component inside a Jinja2 template and wraps it in a<litestar-island>custom element that hydrates on the client according tohydrate="load" | "idle" | "visible" | "media".ComponentResponse(render_fragment()): Returns a standalone pre-rendered HTML fragment (plus scoped<link rel="stylesheet">tags frommanifest.json) directly from a Litestar route handler for HTMX (hx-get/hx-post) partial swaps.
Returning Server-Rendered Fragments for HTMX¶
Route handlers can return ComponentResponse directly. During request handling, Litestar invokes FragmentEngine.render() over the SSR IPC transport (TCPStreamIPCTransport in dev, StdioIPCTransport in prod) and returns text/html:
"""Islands architecture and HTMX server-rendered component fragments."""
from __future__ import annotations
from litestar import Litestar, get
from litestar_vite import ComponentResponse, FragmentEngine, RuntimeConfig, ViteConfig, VitePlugin
@get("/fragments/revenue-chart", sync_to_thread=False)
def revenue_chart_fragment() -> ComponentResponse:
"""Render a standalone UI component fragment for HTMX partial swaps."""
return ComponentResponse(
component="islands/RevenueChart.tsx",
props={"period": "30d", "series": [120, 145, 190, 210]},
headers={"HX-Trigger": "chartRendered"},
)
def create_app() -> Litestar:
"""Create a Litestar app with fragment rendering enabled."""
vite_config = ViteConfig(mode="template", dev_mode=True, runtime=RuntimeConfig(start_dev_server=False))
return Litestar(route_handlers=[revenue_chart_fragment], plugins=[VitePlugin(config=vite_config)])
def main() -> None:
"""Verify ``ComponentResponse`` and ``FragmentEngine`` wiring."""
app = create_app()
assert app is not None
response = ComponentResponse(
component="islands/RevenueChart.tsx", props={"period": "90d", "series": [120, 145, 190, 210]}
)
assert response.component == "islands/RevenueChart.tsx"
assert response.props["period"] == "90d"
assert FragmentEngine is not None
__all__ = ("create_app", "main", "revenue_chart_fragment")
Static vs Island Fragment Modes¶
ComponentResponse / vite_fragment Mode |
Client JavaScript | Output HTML |
|---|---|---|
mode="island" (or hydrate="load" | "idle" | "visible" | "media") |
Hydrates interactive component on client | Wraps pre-rendered HTML in <litestar-island data-component="..." data-hydrate="..."> with serialized props and client entry <script>. |
mode="static" (or hydrate="none") |
Zero client-side hydration | Returns pure server-rendered HTML and scoped <link rel="stylesheet"> tags without <litestar-island> wrapper overhead. |
Embedding Interactive Islands in Jinja2¶
Inside any Jinja2 template, call vite_fragment(component, props=..., hydrate=..., media=...):
{{ vite_fragment(
'islands/RevenueChart.tsx',
props={'period': '30d', 'series': [120, 145, 190, 210]},
hydrate='visible'
) }}
vite_fragment() emits a <litestar-island> custom element containing:
- The server-rendered HTML inside
<div data-island-root>...</div>(so there is zero layout shift before JS loads). <script type="application/json" data-island-props>{...}</script>carrying serialized component props.- Scoped
<link rel="stylesheet">tags resolved frommanifest.jsonin production.
Hydration Strategies¶
| Strategy | Attribute | When the Component Hydrates |
|---|---|---|
"load" (default) |
data-hydrate="load" |
Immediately when <litestar-island> connects to the DOM |
"idle" |
data-hydrate="idle" |
Inside requestIdleCallback() (falls back to 200ms timeout) |
"visible" |
data-hydrate="visible" |
When scrolled within 200px of the viewport (IntersectionObserver) |
"media" |
data-hydrate="media" + data-media="(min-width: 768px)" |
When the CSS media query matches (matchMedia) |
"none" |
data-hydrate="none" |
Server-rendered HTML only; never mounts client JavaScript |
Client-Side Island Registration & HTMX Extension¶
Register <litestar-island> and the Litestar HTMX CSRF/Directive extension (registerHtmxExtension()) in your frontend entrypoint:
import { hydrateIslands } from "litestar-vite-plugin/fragments/client";
import { hydrateReact } from "litestar-vite-plugin/fragments/react";
import { registerHtmxExtension } from "litestar-vite-plugin/helpers";
registerHtmxExtension();
hydrateIslands({
"islands/RevenueChart": {
load: () => import("./islands/RevenueChart"),
hydrate: hydrateReact,
},
});
import { hydrateIslands } from "litestar-vite-plugin/fragments/client";
import { hydrateVue } from "litestar-vite-plugin/fragments/vue";
import { registerHtmxExtension } from "litestar-vite-plugin/helpers";
registerHtmxExtension();
hydrateIslands({
"islands/RevenueChart": {
load: () => import("./islands/RevenueChart.vue"),
hydrate: hydrateVue,
},
});
import { hydrateIslands } from "litestar-vite-plugin/fragments/client";
import { hydrateSvelte } from "litestar-vite-plugin/fragments/svelte";
import { registerHtmxExtension } from "litestar-vite-plugin/helpers";
registerHtmxExtension();
hydrateIslands({
"islands/RevenueChart": {
load: () => import("./islands/RevenueChart.svelte"),
hydrate: hydrateSvelte,
},
});
Production Fragment SSR Worker¶
For production SSR of fragments (dev_mode=False), configure createFragmentRenderer with startSsrWorker in resources/ssr.ts and configure devSsr.componentRoots in vite.config.ts:
import { createFragmentRenderer } from "litestar-vite-plugin/fragments";
import { startSsrWorker } from "litestar-vite-plugin/ssr-worker";
const renderFragment = createFragmentRenderer(
import.meta.glob("./islands/**/*.tsx") as Record<string, () => Promise<unknown>>,
{
clientEntry: "resources/main.ts",
},
);
export default renderFragment;
startSsrWorker({ renderFragment });