UI Islands¶
Islands are self-contained, interactive JavaScript components that mount into server-rendered pages. They let you add client-side interactivity — charts, editors, drag-and-drop — without coupling the entire app to a JavaScript framework.
Overview¶
Dazzle apps are server-rendered with htmx. Most interactions work without JavaScript. But some features (data visualizations, rich editors, real-time updates) need client-side code. Islands solve this by providing designated mount points where JavaScript components take over a specific DOM subtree.
Each island:
- Owns its DOM subtree
- Manages its own state
- Communicates with the server through a defined API contract
- Cleans up when removed from the page (including htmx swaps)
DSL Syntax¶
island <name> "<Title>":
entity: <EntityName>
src: "<path/to/island.js>"
fallback: "<HTML shown before JS loads>"
All fields except name are optional.
Status today: only
entity:has a runtime effect. It mounts the data route described under API Routes. No DSL construct places an island on a page, so the framework does not render any island mount point:title,src:andfallback:are parsed and stored onIslandSpecbut not read, anddazzle validateemits a warning naming DD-011 for each island that declares them. Rendering is parked in DD-011. To mount an island today, emit the mount point yourself (see Mount-point HTML) from a custom route orrender:surface and loaddz-islands.jsvia[ui] app_scripts = ["/static/js/dz-islands.js"]indazzle.toml.
Fields¶
| Field | Required | Runtime effect today |
|---|---|---|
name |
Yes | Unique identifier; names the API route |
title |
No | None (parsed, not rendered — DD-011) |
entity |
No | Generates a data API at /_dazzle/islands/{name}/data |
src |
No | None (parsed, not rendered — DD-011) |
fallback |
No | None (parsed, not rendered — DD-011) |
JavaScript Contract¶
Each island module must export a mount function:
// islands/my-component/index.js
export function mount({ el, props, apiBase }) {
// el: HTMLElement — the <div data-island="..."> mount point
// props: object — parsed `data-island-props` JSON
// apiBase: string — base URL for API endpoints ("" if no entity)
// ... initialize component ...
// Optionally return a cleanup function
return function unmount(el) {
// cleanup timers, listeners, etc.
};
}
The cleanup function is called when:
- The island element is removed from the DOM
- htmx swaps content that contains the island
- The page navigates away
Mount-point HTML¶
The framework does not generate this markup from the DSL yet (DD-011). The
island loader mounts any element of this shape, which is what a custom route
or render: surface emits by hand (the built-in signing pad does the same):
<div data-island="task_chart"
data-island-src="/static/islands/task-chart/index.js"
data-island-props='{"chart_type":"bar","height":400}'
data-island-api-base="/_dazzle/islands/task_chart"
class="dz-island">
Loading task chart...
</div>
The island loader (dz-islands.js) handles:
- Mounting islands on page load
- Rescanning after htmx swaps (
htmx:afterSettle) - Unmounting before htmx replaces content (
htmx:beforeSwap) - Deduplication (prevents double-mounting)
API Routes¶
When an island declares an entity binding, the framework generates:
Response:
No route is created for islands without an entity binding.
Examples¶
Data-Bound Chart¶
entity Task "Task":
id: uuid pk
title: str(200) required
completed: bool = false
island task_chart "Task Progress":
entity: Task
src: "islands/task-chart/index.js"
fallback: "Loading chart..."
// islands/task-chart/index.js
export async function mount({ el, props, apiBase }) {
const response = await fetch(`${apiBase}/data`);
const data = await response.json();
renderChart(el, {
items: data.items,
});
}
Simple Effect (No Entity)¶
island confetti "Celebration Confetti":
src: "islands/confetti/index.js"
fallback: "<p>Celebration!</p>"
export function mount({ el }) {
const button = document.createElement("button");
button.textContent = "Celebrate";
button.addEventListener("click", () => launchConfetti());
el.appendChild(button);
return () => button.remove();
}
File Structure¶
Islands follow this layout:
static/islands/
my-component/
index.js # ES module with mount() export
styles.css # Scoped styles (optional)
Best Practices¶
- Return cleanup functions — clear timers, remove listeners, abort fetches
- Handle errors — wrap fetch calls and show fallback UI on failure
- Scope styles — use BEM or CSS modules to avoid global collisions
- Keep islands focused — one responsibility per island
- Use progressive enhancement — provide meaningful
fallbackcontent - Lazy load dependencies — use dynamic
import()for large libraries - Document events — declare all CustomEvents in the DSL for discoverability