hx-optimistic
v1.0.11
Published
HTMX extension for optimistic UI updates with automatic rollback on errors
Downloads
64
Maintainers
Readme
hx-optimistic
An htmx extension for optimistic UI updates with automatic rollback on errors. Combine it with speculation rules (or the htmx preload extension) and the View Transitions API for truly app‑like experience with minimal JavaScript. We love JavaScript, but use it like a spice: a pinch delights, too much overwhelms.
✨ Features
- 🎯 Optimistic Updates - Immediate UI feedback while requests are processing
- 🔄 Automatic Rollback - Intelligent revert to original state on errors
- 📝 Input Interpolation - Dynamic templates with
${this.value},${textarea},${data:key}helpers - 🎨 Template Support - Rich HTML templates for loading and error states
- ⚠️ Developer Warnings - Console warnings for unsupported patterns
- 🚫 No CSS Required - You control all styling through provided class names
- 📦 Tiny - Only 18.2KB uncompressed, 8.4KB minified, 3.0KB gzipped
- 🔧 Highly Configurable - Fine-tune behavior per element
🚀 Quick Start
Installation
Via CDN (jsDelivr, pinned major):
<script defer src="https://unpkg.com/htmx.org@2"></script>
<script defer src="https://cdn.jsdelivr.net/npm/hx-optimistic@1/hx-optimistic.min.js"></script>Alternative (unpkg, latest v1):
<script defer src="https://unpkg.com/htmx.org@2"></script>
<script defer src="https://unpkg.com/hx-optimistic@1/hx-optimistic.min.js"></script>Compatibility: Works with htmx 1.9+ and 2.x.
Via NPM:
npm install hx-optimisticBasic Usage
Enable the extension and add optimistic behavior to any HTMX element:
<body hx-ext="optimistic">
<!-- Simple like button with optimistic updates -->
<button
hx-post="/api/like"
hx-target="this"
hx-swap="outerHTML"
data-optimistic='{"values":{"textContent":"❤️ Liked!","className":"btn liked"},"errorMessage":"Failed to like"}'
>
🤍 Like
</button>
</body>🎯 Core Concepts
- Use
data-optimisticto declare behavior per element - Choose between
values(simple property changes) andtemplate(full HTML) - Automatic snapshot and revert:
innerHTML,className, attributes, anddataset - Token-based concurrency prevents stale errors from overwriting newer states
- Target resolution via
hx-targetor configtargetchains:closest,find,next,previous - Errors via
errorMessageorerrorTemplatewitherrorModeanddelay - Interpolation supports safe patterns only; avoid arbitrary JS expressions
📦 Bundle Size
| Artifact | Size |
|---------|------|
| Unminified (hx-optimistic.js) | 18.2 KB |
| Minified (hx-optimistic.min.js) | 8.4 KB |
| Minified + gzip | 3.0 KB |
Values vs Templates
Values - Perfect for simple optimistic updates:
<button
data-count="42"
data-liked="false"
hx-post="/api/like"
hx-target="this"
hx-swap="outerHTML"
data-optimistic='{
"values": {
"textContent": "❤️ Liked! (was ${data:count})",
"className": "btn liked",
"data-liked": "true"
}
}'>
🤍 Like (42)
</button>Templates - Ideal for complex optimistic UI changes:
<form hx-post="/api/comments" hx-target=".comments" hx-swap="beforeend"
data-optimistic='{
"template": "<div class='comment optimistic'><strong>You:</strong> ${textarea}</div>",
"errorTemplate": "<div class='error'>❌ Comment failed to post</div>"
}'>
<textarea name="comment" placeholder="Your comment here"></textarea>
<button type="submit">Post Comment</button>
</form>Input Interpolation
Dynamic content using ${...} syntax with powerful helpers:
<form hx-post="/api/comments" hx-ext="optimistic"
data-optimistic='{"template":"<div>Posting: ${textarea}</div>"}'>
<textarea name="comment">Your comment here</textarea>
<button type="submit">Post</button>
</form>📖 Interpolation Reference
All ${...} patterns supported in templates and values:
| Pattern | Description | Example |
|---------|-------------|---------|
| ${this.value} | Element's input value | "Saving: ${this.value}" |
| ${this.textContent} | Element's text content | "Was: ${this.textContent}" |
| ${this.dataset.key} | Data attribute via dataset | "ID: ${this.dataset.userId}" |
| ${textarea} | First textarea in form | "Comment: ${textarea}" |
| ${email} | First email input | "Email: ${email}" |
| ${data:key} | Data attribute shorthand | "Count: ${data:count}" |
| ${attr:name} | Any HTML attribute | "ID: ${attr:id}" |
| ${contextKey} | Value from config.context (templates only) | "Hello, ${username}" |
| ${status} | HTTP status (errors only) | "Error ${status}" |
| ${statusText} | HTTP status text (errors only) | "Error: ${statusText}" |
| ${error} | Error message (errors only) | "Failed: ${error}" |
Form Field Helpers:
${textarea},${email},${password},${text},${url},${tel},${search}${fieldName}- Any field withname="fieldName"
⚙️ Configuration Options
Complete configuration reference for data-optimistic:
Snapshot Behavior
innerHTML, className, all attributes, and the element dataset are automatically captured and restored on revert; no configuration is required.
Optimistic Updates
{
// Simple property updates
"values": {
"textContent": "Loading...",
"className": "btn loading"
},
// Rich HTML templates
"template": "#loading-template", // Or inline HTML
"target": "closest .card", // Different target for optimistic update
"swap": "beforeend", // Append instead of replace
"class": "my-optimistic" // Optional custom class applied during optimistic state
}swap supports beforeend and afterbegin. If omitted, content is replaced.
Error Handling
{
"errorMessage": "Request failed",
"errorTemplate": "<div class='error'>Error ${status}: ${statusText}</div>",
"errorMode": "append", // "replace" (default) or "append"
"delay": 2000 // Auto-revert delay in ms
}Context Data
Provide additional variables for template interpolation:
{
"template": "<div>Hello, ${username}</div>",
"context": { "username": "Alice" }
}Full example:
<div class="profile-card">
<h3>Alex</h3>
<p>Status: 🔴 Offline</p>
<button
hx-post="/api/status"
hx-target="closest .profile-card"
hx-swap="outerHTML"
hx-ext="optimistic"
data-optimistic='{"template":"#profile-next","errorTemplate":"#profile-error","errorMode":"append","context":{"username":"Alex","nextStatus":"Online","nextIcon":"🟢"}}'
>
Change Status
</button>
</div>
<template id="profile-next">
<div class="profile-card">
<h3>${username}</h3>
<p>Status: ${nextIcon} ${nextStatus}</p>
</div>
</template>
<template id="profile-error">
<div class="error">❌ ${error}</div>
</template>🎨 CSS Classes
This library does not include any CSS. These classes are applied so you can style them as you wish:
hx-optimistic: applied during the optimistic updatehx-optimistic-error: applied when an error is shownhx-optimistic-reverting: applied while reverting to the snapshothx-optimistic-error-message: wrapper added when errorMode is "append"hx-optimistic-pending: may be applied to<button>when novalues/templateare provided
✅ Best Practices
- Enable globally when possible: Add
hx-ext="optimistic"on<body>so elements inherit it. Use per-elementhx-extonly when you need to opt-in selectively. - Pick the right technique:
- values: simple property changes (
textContent,className,data-*). - template: richer markup; prefer a
<template id="...">and reference it with"#id".
- values: simple property changes (
- Keep interpolation simple: Only supported patterns are documented (e.g.,
${this.value},${textarea},${data:key},${attr:name}). Avoid expressions like${count + 1}; usedata-*/hx-valsto pass values. - Design error UX: Provide
errorMessageorerrorTemplate. UseerrorMode: "append"to preserve content; setdelay(ms) for auto-revert, ordelay: 0to keep the error state. - Target resolution: Use
hx-targetor configtargetwith chains likeclosest .card find .title. Supported ops:closest,find,next,previous. Prefer stable selectors over brittle DOM traversal. - Style the states: Add styles for
hx-optimistic,hx-optimistic-error,hx-optimistic-reverting, andhx-optimistic-error-message, or provide a customclassin config. - Concurrency is automatic: Overlapping requests are tokenized; older errors won’t clobber newer optimistic states. Avoid writing concurrency flags into
dataset. - Snapshots are automatic:
innerHTML,className, attributes, and dataset are restored automatically on revert; no custom snapshot configuration is needed. - Pass extra data via context: Use
contextto provide additional variables to templates. Error templates also receive${status},${statusText}, and${error}. - Accessibility: The extension preserves focus within the target after error/revert. Ensure visible focus styles and consider ARIA live regions for error messages.
- Diagnostics: Watch the console for warnings about unresolved selectors/templates or unsupported interpolation patterns, and fix the sources accordingly.
- Default button behavior: If
data-optimisticis present on a<button>withoutvaluesortemplate, ahx-optimistic-pendingclass is added during the request.
📚 Examples
See usage snippets above for common patterns.
🔧 Developer Features
Console Warnings
The extension provides helpful warnings for unsupported patterns:
// ❌ These will show console warnings
"${this.querySelector('.test')}" // DOM queries not allowed
"${window.location.href}" // Global object access
"${JSON.parse(data)}" // Method calls not supported
// ✅ These are supported
"${data:user-id}" // Data attributes
"${attr:title}" // HTML attributes
"${this.value}" // Element propertiesLifecycle Events
Three custom events are dispatched on the optimistic target. Use event delegation to observe them:
<script>
document.body.addEventListener('optimistic:applied', (e) => {
const target = e.target;
const { config } = e.detail;
// handle start of optimistic state
});
document.body.addEventListener('optimistic:error', (e) => {
const target = e.target;
const { config, detail: errorData } = e.detail; // { status, statusText, error }
// handle error state
});
document.body.addEventListener('optimistic:reverted', (e) => {
const target = e.target;
const { config } = e.detail;
// handle completion of revert
});
</script>If you prefer htmx utilities:
<script>
htmx.on(document.body, 'optimistic:applied', (e) => { /* ... */ });
htmx.on(document.body, 'optimistic:error', (e) => { /* ... */ });
htmx.on(document.body, 'optimistic:reverted', (e) => { /* ... */ });
</script>Template References
Use <template> elements for better organization:
<div class="comments"></div>
<template id="comment-preview">
<div class="comment"><strong>You:</strong> ${textarea}</div>
</template>
<template id="comment-error">
<div class="error">❌ ${error}</div>
</template>
<form hx-post="/api/comments" hx-ext="optimistic" hx-target=".comments" hx-swap="beforeend"
data-optimistic='{"template":"#comment-preview","errorTemplate":"#comment-error","errorMode":"append"}'>
<textarea name="comment" placeholder="Your comment here"></textarea>
<button type="submit">Post Comment</button>
</form>🎮 Live Demo
🤝 Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature-name - Run tests:
npm test - Make your changes
- Submit a pull request
📦 Release
Tag-based releases trigger npm publish in CI:
- Update version in
package.jsonif needed - Create a tag and push it:
git tag v1.0.0
git push origin v1.0.0- Ensure
NPM_TOKENis set in GitHub Actions secrets
📄 License
MIT License - see LICENSE for details.
hx-optimistic - Making HTMX interactions feel instant with intelligent optimistic updates. ⚡
