HTMX
Build interactive applications with HTMX's HTML-over-the-wire model and native fragment updates.
#Build Configuration
Add HTMX to your build by specifying the directory containing your HTMX pages:
const manifest = await build({
htmxDirectory: 'src/htmx/pages'
});#Page Handler
Pass the path to the built HTML file to handleHTMXPageRequest. HTMX interactivity stays in the document and your endpoints, not in a page-local sidecar.
// backend/server.ts
import { handleHTMXPageRequest } from '@absolutejs/absolute';
new Elysia()
.get('/app', () =>
handleHTMXPageRequest('./build/pages/app.html')
)#Example
HTMX pages use HTML attributes to trigger server requests and update the DOM:
<!-- src/htmx/pages/app.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>HTMX App</title>
<script src="/htmx/htmx.min.js"></script>
<link rel="stylesheet" href="./styles/htmx-app.css">
</head>
<body>
<h1>HTMX Application</h1>
<button hx-get="/api/data" hx-target="#results" hx-swap="innerHTML">
Load Data
</button>
<div id="results"></div>
</body>
</html>#API Endpoints
HTMX requests are handled by regular Elysia endpoints that return HTML fragments:
// HTMX requests return HTML fragments
new Elysia()
.get('/api/data', () => {
const items = getItems();
return `
<ul>
${items.map(item => `<li>${item.name}</li>`).join('')}
</ul>
`;
})#Out-of-Order Streaming
HTMX gets its own first-class primitive: <abs-htmx-stream-slot>. AbsoluteJS lowers each tag into native HTMX markup, so the browser still performs normal hx-get fragment requests and swaps in the returned HTML when that endpoint finishes.
<!-- src/htmx/pages/dashboard.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>HTMX Live Dashboard</title>
<script src="/htmx/htmx.min.js"></script>
</head>
<body>
<section class="grid">
<abs-htmx-stream-slot src="/dashboard/cards/summary">
<article class="card card-fallback">
<h2>Summary</h2>
<p>Loading...</p>
</article>
</abs-htmx-stream-slot>
<abs-htmx-stream-slot src="/dashboard/cards/activity">
<article class="card card-fallback">
<h2>Activity</h2>
<p>Loading...</p>
</article>
</abs-htmx-stream-slot>
</section>
</body>
</html>The server stays explicit: serve the page with handleHTMXPageRequest and expose one fragment endpoint per region. The authored HTMX page stays declarative, while AbsoluteJS handles the lowering step.
import { Elysia } from 'elysia';
import { handleHTMXPageRequest } from '@absolutejs/absolute';
const delay = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
new Elysia()
.get('/dashboard', () =>
handleHTMXPageRequest('./build/pages/dashboard.html')
)
.get('/dashboard/cards/summary', async () => {
await delay(500);
return '<article class="card"><h2>Summary</h2><p>Resolved first</p></article>';
})
.get('/dashboard/cards/activity', async () => {
await delay(1800);
return '<article class="card"><h2>Activity</h2><p>Resolved later</p></article>';
});
// AbsoluteJS lowers each <abs-htmx-stream-slot>
// to hx-get + hx-trigger="load" + hx-swap="outerHTML".#Per-User State with Scoped State
When building interactive HTMX applications, you often need state that's specific to each user. For example, a counter button should only increment that user's count, not everyone's. The @absolutejs/scoped-state plugin solves this by automatically managing per-user sessions.
Without scoped state, all users would share the same server state. With scoped state, each user gets their own isolated state slice:
import Elysia from 'elysia';
import { scopedState } from '@absolutejs/scoped-state';
import { handleHTMXPageRequest } from '@absolutejs/absolute';
new Elysia()
.use(
scopedState({
count: { value: 0 },
cart: { value: [] }
})
)
.get('/app', () => handleHTMXPageRequest('./build/pages/app.html'))
.get('/api/count', ({ scopedStore }) => {
// Returns this user's count only
return `<span>${scopedStore.count}</span>`;
})
.post('/api/increment', ({ scopedStore }) => {
// Only increments this user's count
return `<span>${++scopedStore.count}</span>`;
})
.listen(3000);The HTML stays the same, but now each user's interactions only affect their own state:
<button hx-post="/api/increment" hx-target="#count" hx-swap="outerHTML">
Increment
</button>
<span id="count" hx-get="/api/count" hx-trigger="load">
Loading...
</span>
<!-- User A clicks 5 times → sees 5 -->
<!-- User B visits the page → sees 0 (their own fresh state) -->
<!-- User B clicks 2 times → sees 2 (independent from User A) -->