Enable htmx attributes and extension script loaders.
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.
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))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> *)<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>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>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
/>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>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>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>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+"
/>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><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><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><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 />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>