Enable htmx attributes and extension script loaders.
How it works
The -htmx flag extends the set of valid attributes on every HTML element. There is no separate API — you keep writing normal JSX, and attributes like hx_get, hx_post, hx_swap, etc. become available alongside the standard HTML ones. The ppx validates them at compile time just like any other attribute.
Enable mode
Turn on htmx attributes in the ppx and include the runtime helper package.
(libraries html_of_jsx html_of_jsx.htmx)
(preprocess (pps html_of_jsx.ppx -htmx))
Core attributes
Use hx_* names in JSX. They render as standard hx-* HTML attributes.
JSX.render(<a hx_get="/profile" hx_swap=`outerHTML> {JSX.string("Load profile")} </a>)
/* <a hx-get="/profile" hx-swap="outerHTML">Load profile</a> */
JSX.render <a hx_get="/profile" hx_swap=`outerHTML> (JSX.string "Load profile") </a>
(* <a hx-get="/profile" hx-swap="outerHTML">Load profile</a> *)
Request methods
<button hx_get="/items"> {JSX.string("Load")} </button>
<form hx_post="/submit"> {children} </form>
<button hx_delete={"/items/" ++ id}> {JSX.string("Remove")} </button>
<button hx_put={"/items/" ++ id}> {JSX.string("Replace")} </button>
<button hx_patch={"/items/" ++ id}> {JSX.string("Update")} </button>
<button hx_get="/items"> (JSX.string "Load") </button>
<form hx_post="/submit"> children </form>
<button hx_delete={"/items/" ++ id}> (JSX.string "Remove") </button>
<button hx_put={"/items/" ++ id}> (JSX.string "Replace") </button>
<button hx_patch={"/items/" ++ id}> (JSX.string "Update") </button>
Targeting and swapping
Control where the response is placed and how it replaces existing content:
<button hx_get="/content" hx_target="#result" hx_swap=`innerHTML>
{JSX.string("Load into #result")}
</button>
<button hx_get="/row" hx_target="closest tr" hx_swap=`outerHTML>
{JSX.string("Replace this row")}
</button>
<button hx_get="/content" hx_target="#result" hx_swap=`innerHTML>
(JSX.string "Load into #result")
</button>
<button hx_get="/row" hx_target="closest tr" hx_swap=`outerHTML>
(JSX.string "Replace this row")
</button>
Triggers
Customize what event fires the request. Supports modifiers like delay, changed, and throttle:
<input
type_=`search
name="query"
hx_post="/search"
hx_trigger="keyup changed delay:300ms, search"
hx_target="#results"
hx_swap=`outerHTML
/>
<input
type_=`search
name="query"
hx_post="/search"
hx_trigger="keyup changed delay:300ms, search"
hx_target="#results"
hx_swap=`outerHTML
/>
Confirmation dialogs
Ask the user to confirm before sending:
<button
hx_delete={Printf.sprintf("/todos/%d", id)}
hx_target={Printf.sprintf("#todo-%d", id)}
hx_swap=`outerHTML
hx_confirm="Are you sure?">
{JSX.string("Delete")}
</button>
<button
hx_delete=(Printf.sprintf "/todos/%d" id)
hx_target=(Printf.sprintf "#todo-%d" id)
hx_swap=`outerHTML
hx_confirm="Are you sure?">
(JSX.string "Delete")
</button>
Loading indicators
Show a spinner or indicator while the request is in flight:
<button hx_get="/slow-content" hx_indicator="#spinner">
{JSX.string("Load")}
</button>
<span id="spinner" class_="htmx-indicator"> {JSX.string("Loading...")} </span>
<button hx_get="/slow-content" hx_indicator="#spinner">
(JSX.string "Load")
</button>
<span id="spinner" class_="htmx-indicator">(JSX.string "Loading...")</span>
Counter example
A server-side counter using hx_post with hx_target and hx_swap:
let counter = (~count, ()) => {
<div style="display: flex; align-items: center; gap: 12px">
<button
hx_post="/counter/decrement"
hx_target="#counter"
hx_swap=`outerHTML>
{JSX.string("-")}
</button>
<span id="counter"> {JSX.int(count)} </span>
<button
hx_post="/counter/increment"
hx_target="#counter"
hx_swap=`outerHTML>
{JSX.string("+")}
</button>
</div>;
};
let counter ~count () =
<div style="display: flex; align-items: center; gap: 12px">
<button
hx_post="/counter/decrement"
hx_target="#counter"
hx_swap=`outerHTML>
(JSX.string "-")
</button>
<span id="counter">(JSX.int count)</span>
<button
hx_post="/counter/increment"
hx_target="#counter"
hx_swap=`outerHTML>
(JSX.string "+")
</button>
</div>
Load script
Add <Htmx /> in <head> to load htmx from unpkg.
<head>
<Htmx version="2.0.4" />
</head>
Pass integrity to enable SRI.
<Htmx
version="2.0.4"
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
/>
Extensions
Use components under Htmx.Extensions for extension scripts. Each extension adds its own attributes to the JSX vocabulary.
<head>
<Htmx version="2.0.4" />
<Htmx.Extensions.SSE version="2.2.2" />
<Htmx.Extensions.WS version="2.2.0" />
</head>
SSE (Server-Sent Events)
<div hx_ext="sse" sse_connect="/events" sse_swap="message">
{JSX.string("Waiting for events...")}
</div>
<div hx_ext="sse" sse_connect="/events" sse_swap="message">
(JSX.string "Waiting for events...")
</div>
WebSocket
<div hx_ext="ws" ws_connect="/chat">
<form ws_send="">
<input name="message" />
<button type_=`submit> "Send" </button>
</form>
</div>
<div hx_ext="ws" ws_connect="/chat">
<form ws_send="">
<input name="message" />
<button type_=`submit> (JSX.string "Send") </button>
</form>
</div>
Available extension loaders
<Htmx.Extensions.SSE /><Htmx.Extensions.WS /><Htmx.Extensions.Class_tools /><Htmx.Extensions.Preload /><Htmx.Extensions.Path_deps /><Htmx.Extensions.Loading_states /><Htmx.Extensions.Response_targets /><Htmx.Extensions.Head_support />
The entire page
A minimal page combining core htmx attributes with an SSE extension:
let page = () => {
<html lang="en">
<head>
<title> {JSX.string("htmx + html_of_jsx")} </title>
<Htmx version="2.0.4" />
<Htmx.Extensions.SSE version="2.2.2" />
</head>
<body>
<button hx_get="/clicked" hx_swap=`outerHTML>
{JSX.string("Click me")}
</button>
<div hx_ext="sse" sse_connect="/events" sse_swap="message">
{JSX.string("Waiting for events...")}
</div>
</body>
</html>
};
let page () =
<html lang="en">
<head>
<title>(JSX.string "htmx + html_of_jsx")</title>
<Htmx version="2.0.4" />
<Htmx.Extensions.SSE version="2.2.2" />
</head>
<body>
<button hx_get="/clicked" hx_swap=`outerHTML>
(JSX.string "Click me")
</button>
<div hx_ext="sse" sse_connect="/events" sse_swap="message">
(JSX.string "Waiting for events...")
</div>
</body>
</html>