The hx-optimistic extension shows a preview of the expected result immediately when a request is made, before the server responds. When the response arrives (or on error), the optimistic content is removed and replaced with the real content.
Installing
<script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/ext/hx-optimistic.js"></script>
Add hx-optimistic to your extensions whitelist:
<meta name="htmx-config" content='extensions:"hx-optimistic"'>
Usage
Add hx-optimistic to any element that makes a request, pointing to a template element:
<ul id="messages"> <li>Hello world</li> </ul> <template id="msg-opt"> <li>Sending...</li> </template> <form hx-post="/message" hx-target="#messages" hx-swap="beforeend" hx-optimistic="#msg-opt"> <input name="body" placeholder="Message..."> <button type="submit">Send</button> </form>
When the form submits, the template content is immediately inserted into the target. When the server responds, the optimistic content is removed and the real response is swapped in.
Request Parameters as Data Attributes
The extension captures all string request parameters (form inputs, hx-vals, hx-include) and sets them as data-* attributes on the optimistic element. This makes the submitted values available to CSS and to the hx-live extension.
For a form with <input name="author" value="You">, the optimistic div gets data-author="You".
Multi-value fields (checkboxes, multi-selects) are stored as JSON arrays: data-tags='["js","css"]'.
Dynamic Templates with hx-live
When used with the hx-live extension, optimistic templates can display the submitted values using :text bindings and the data proxy:
<template id="msg-opt"> <li><strong :text="data.author"></strong>: <span :text="data.body"></span></li> </template> <form hx-post="/message" hx-target="#messages" hx-swap="beforeend" hx-optimistic="#msg-opt"> <input name="author" value="You"> <input name="body" placeholder="Message..."> <button type="submit">Send</button> </form>
The data proxy reads data-* attributes from the nearest ancestor, and hx-live’s :text binding sets textContent safely (no XSS risk). Full JavaScript expressions are supported:
<template id="order-opt"> <div> <span :text="data.item"></span> <span :text="'$' + (data.price * data.qty).toFixed(2)"></span> <span :text="Array.isArray(data.tags) ? data.tags.join(', ') : data.tags"></span> </div> </template>
Styling
The optimistic element receives a hx-optimistic class, which you can style with CSS:
.hx-optimistic { opacity: 0.6; font-style: italic; } .hx-optimistic::after { content: " (sending...)"; font-size: 0.8em; color: #888; }
You can also style based on the request parameters:
.hx-optimistic[data-priority="high"] { border-left: 3px solid red; }
How It Works
- On
htmx:config:request— captures the rawFormDatabefore htmx transforms it - On
htmx:before:request— clones the template, setsdata-*for each param, inserts it into the target (respecting swap style), and callshtmx.process()so hx-live bindings activate - On
htmx:before:swaporhtmx:error— removes the optimistic content and unhides any hidden elements
Swap Style Behavior
The extension respects the hx-swap value:
| Swap Style | Behavior |
|---|---|
innerHTML | Hides target’s children, appends optimistic content |
beforebegin, afterbegin, beforeend, afterend | Inserts optimistic content at that position |
outerHTML / other | Hides target, inserts optimistic content after it |
Without hx-live
The extension works without hx-live — static template content displays as-is, and the data-* attributes are still available for CSS selectors. You only need hx-live if you want dynamic :text / :class / :style bindings in the template.