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.

dune
(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.

Reason
JSX.render(<a hx_get="/profile" hx_swap=`outerHTML> {JSX.string("Load profile")} </a>)

/* <a hx-get="/profile" hx-swap="outerHTML">Load profile</a> */
mlx
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

Reason
<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>
mlx
<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:

Reason
<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>
mlx
<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:

Reason
<input

  type_=`search

  name="query"

  hx_post="/search"

  hx_trigger="keyup changed delay:300ms, search"

  hx_target="#results"

  hx_swap=`outerHTML

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

Reason
<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>
mlx
<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:

Reason
<button hx_get="/slow-content" hx_indicator="#spinner">

  {JSX.string("Load")}

</button>

<span id="spinner" class_="htmx-indicator"> {JSX.string("Loading...")} </span>
mlx
<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:

Reason
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>;

};
mlx
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.

Reason
<head>

  <Htmx version="2.0.4" />

</head>

Pass integrity to enable SRI.

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

Reason
<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)

Reason
<div hx_ext="sse" sse_connect="/events" sse_swap="message">

  {JSX.string("Waiting for events...")}

</div>
mlx
<div hx_ext="sse" sse_connect="/events" sse_swap="message">

  (JSX.string "Waiting for events...")

</div>

WebSocket

Reason
<div hx_ext="ws" ws_connect="/chat">

  <form ws_send="">

    <input name="message" />

    <button type_=`submit> "Send" </button>

  </form>

</div>
mlx
<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

The entire page

A minimal page combining core htmx attributes with an SSE extension:

Reason
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>

};
mlx
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>