AbsoluteJS
AbsoluteJS

HTMX

Build interactive applications with HTMX's HTML-over-the-wire approach.

#

Build Configuration

Add HTMX to your build by specifying the directory containing your HTMX pages:

TS
1const manifest = await build({
2  htmxDirectory: 'src/htmx/pages'
3});
#

Page Handler

Pass the path to the built HTML file to handleHTMXPageRequest:

TS
1// backend/server.ts
2import { handleHTMXPageRequest } from '@absolutejs/absolute';
3
4new Elysia()
5  .get('/app', () =>
6    handleHTMXPageRequest('./build/pages/app.html')
7  )
#

Example

HTMX pages use HTML attributes to trigger server requests and update the DOM:

HTML
1<!-- src/htmx/pages/app.html -->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5  <title>HTMX App</title>
6  <script src="https://unpkg.com/[email protected]"></script>
7  <link rel="stylesheet" href="./styles/htmx-app.css">
8</head>
9<body>
10  <h1>HTMX Application</h1>
11
12  <button hx-get="/api/data" hx-target="#results">
13    Load Data
14  </button>
15
16  <div id="results"></div>
17
18  <script src="./scripts/htmx-helpers.ts"></script>
19</body>
20</html>
#

API Endpoints

HTMX requests are handled by regular Elysia endpoints that return HTML fragments:

TS
1// HTMX requests return HTML fragments
2new Elysia()
3  .get('/api/data', () => {
4    const items = getItems();
5
6    return `
7      <ul>
8        ${items.map(item => `<li>${item.name}</li>`).join('')}
9      </ul>
10    `;
11  })
  • Return HTML: Endpoints return HTML strings that HTMX injects into the page
  • Type-safe data: Your data fetching is still fully typed even though the response is HTML
  • No JSON serialization: Skip the JSON/parse cycle for simpler data flow
#

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 elysia-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
1import Elysia from 'elysia';
2import { scopedState } from 'elysia-scoped-state';
3import { handleHTMXPageRequest } from '@absolutejs/absolute';
4
5new Elysia()
6  .use(
7    scopedState({
8      count: { value: 0 },
9      cart: { value: [] }
10    })
11  )
12  .get('/app', () => handleHTMXPageRequest('./build/pages/app.html'))
13  .get('/api/count', ({ scopedStore }) => {
14    // Returns this user's count only
15    return `<span>${scopedStore.count}</span>`;
16  })
17  .post('/api/increment', ({ scopedStore }) => {
18    // Only increments this user's count
19    return `<span>${++scopedStore.count}</span>`;
20  })
21  .listen(3000);

The HTML stays the same, but now each user's interactions only affect their own state:

HTML
1<!-- Each user's button clicks only affect their own count -->
2<div>
3  Count: <span id="count" hx-get="/api/count" hx-trigger="load">0</span>
4</div>
5
6<button hx-post="/api/increment" hx-target="#count" hx-swap="innerHTML">
7  +1
8</button>
9
10<!-- User A clicks 5 times → sees 5 -->
11<!-- User B visits the page → sees 0 (their own fresh state) -->
12<!-- User B clicks 2 times → sees 2 (independent from User A) -->
  • Automatic sessions: A secure user_session_id cookie is created on first visit
  • User isolation: Each user's scopedStore is completely independent
  • No client-side state: All state lives on the server, perfect for HTMX's HTML-over-the-wire approach

See the Scoped State documentation for more details on configuration options and advanced usage.