AbsoluteJS

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:

TS
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.

TS
// 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:

HTML
<!-- 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:

TS
// HTMX requests return HTML fragments
new Elysia()
  .get('/api/data', () => {
    const items = getItems();

    return `
      <ul>
        ${items.map(item => `<li>${item.name}</li>`).join('')}
      </ul>
    `;
  })
Return HTMLEndpoints return HTML strings that HTMX injects into the page
Type-safe dataYour data fetching is still fully typed even though the response is HTML
No JSON serializationSkip the JSON/parse cycle for simpler data flow

#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.

HTML
<!-- 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.

TS
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:

TS
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:

HTML
<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) -->
Automatic sessionsA secure user_session_id cookie is created on first visit
User isolationEach user's scopedStore is completely independent