hx-optimistic Demo

Comprehensive demonstration of optimistic updates with HTMX, featuring interpolation helpers, developer warnings, and enhanced error handling.

Documentation

V1

Quick start and key patterns for the optimistic HTMX extension. The demos below showcase each pattern.

Enable the extension

<script src="https://unpkg.com/htmx.org@2"></script>
<script src="https://unpkg.com/hx-optimistic@1/hx-optimistic.min.js"></script>
<body hx-ext="optimistic">...</body>

Values pattern

<button
  hx-post="/api/like"
  hx-target="this"
  hx-swap="outerHTML"
  hx-ext="optimistic"
  data-optimistic='{"snapshot":["textContent"],"values":{"textContent":"Saving..."},"delay":2000}'>
  Like
</button>

Template pattern

<button
  hx-post="/api/counter"
  hx-target="closest .counter"
  hx-swap="outerHTML"
  hx-ext="optimistic"
  data-optimistic='{"template":"<div class=\'counter hx-optimistic\'>...</div>","errorMessage":"Failed"}'>
  +
</button>

Error handling

{"errorMessage":"Could not save","delay":2000}

GitHub repository

Like Button with Toggle

Click to like/unlike. The count increments when liked, decrements when unliked. 20% chance of simulated errors to demonstrate error handling.

Look for the optimistic styling: When clicked, the button briefly shows a blue background with blue outline while the request is processed. On errors, you'll see a red background with red outline before reverting.
Demo behavior: stateless; you can like once and then unlike once.
Uses hx-swap="outerHTML" with wrapper container to prevent layout shifts.
Features demonstrated:
  • Pre-calculated optimistic values
  • Multiple attribute snapshots (textContent, className, data-*)
  • Toggle logic with conditional states
  • Error handling with automatic revert
  • Layout-stable wrapper technique to prevent shifts during outerHTML swaps
❤️ Interactive

How This Works

1. Layout-Stable Wrapper

<!-- Wrapper with min-height prevents layout shifts -->
<div style="min-height: 120px;">
  <LikeButton liked={false} count={42} />
</div>

2. LikeButton Component Configuration

// Pre-calculate the optimistic state
const toggledLiked = !liked;
const toggledCount = toggledLiked ? count + 1 : count - 1;
const toggledText = toggledLiked ? `❤️ Liked! (${toggledCount})` : `🤍 Like (${toggledCount})`;

const optimisticConfig = {
  snapshot: ["textContent", "className", "data-liked", "data-count"],
  values: {
    textContent: toggledText,
    className: toggledClass,
    "data-liked": toggledLiked.toString(),
    "data-count": toggledCount.toString()
  },
  errorMessage: "Could not update like",
  delay: 2000
};

3. Button HTML

<button 
  class={`btn ${liked ? 'btn-error' : 'btn-outline'}`}
  style="min-width: 140px; width: 140px; white-space: nowrap;"
  data-liked={liked}
  data-count={count}
  hx-post="/api/like"
  hx-target="this"
  hx-swap="outerHTML"
  data-optimistic={JSON.stringify(optimisticConfig)}>
  {liked ? '❤️' : '🤍'} {liked ? 'Liked!' : 'Like'} ({count})
</button>

Status Toggle with Rich Templates

Click to cycle through user status (Online → Away → Offline → Online). 20% chance of simulated errors to demonstrate error handling.

Look for the optimistic update: next status with blue border on click; red-bordered error before reverting.
Features demonstrated:
  • Complete HTML template replacement
  • Separate error templates
  • Full content snapshotting
  • Template interpolation with component data
  • Layout-stable wrapper technique to prevent shifts during outerHTML swaps
🟢 Interactive
A

Alex

🟢 Online

How This Works

1. Layout-Stable Wrapper

<!-- Wrapper with min-height prevents layout shifts -->
<div style="min-height: 200px;">
  <StatusToggle status="online" username="Alex" />
</div>

2. Optimistic Templates with Consistent Dimensions

<template id="status-optimistic-template">
  <div class="status-card card bg-base-100 shadow-md border-2 border-base-300 hx-optimistic">
    <div class="card-body p-4">
      <!-- Mobile-friendly layout with consistent dimensions -->
      <div class="flex flex-col gap-3">
        <div class="flex items-center gap-3">
          <div class="avatar placeholder">
            <div class="bg-neutral text-neutral-content rounded-full w-12 h-12">
              <span class="text-xl">$&#123;data:initials&#125;</span>
            </div>
          </div>
          <div class="flex-1 min-w-0">
            <h3 class="font-semibold">$&#123;data:username&#125;</h3>
            <div class="flex items-center gap-2">
              <span class="text-lg">$&#123;data:nextIcon&#125;</span>
              <span class="badge badge-$&#123;data:nextColor&#125;">$&#123;data:nextText&#125;</span>
            </div>
          </div>
        </div>
        <button class="btn btn-outline btn-sm w-full sm:w-auto">Change Status</button>
      </div>
    </div>
  </div>
</template>

3. Button Configuration

<button
  class="btn btn-outline btn-sm"
  hx-post="/api/status"
  hx-target="closest .status-card"
  hx-swap="outerHTML"
  hx-ext="optimistic"
  data-username={username}
  data-initials={username.charAt(0).toUpperCase()}
  data-next-icon={nextConfig.icon}
  data-next-color={nextConfig.color}
  data-next-text={nextConfig.text}
  hx-vals={JSON.stringify({ username, currentStatus: status, newStatus: nextStatus })}
  data-optimistic={JSON.stringify(optimisticConfig)}
>
  Change Status
</button>

Comment System with Input Interpolation

Post a comment and see it appear immediately with optimistic updates. 25% chance of simulated errors to demonstrate append-mode error handling.

Look for input interpolation: your comment text appears immediately via ${textarea}.
Features demonstrated:
  • User input interpolation with ${textarea}
  • Append error mode (errorMode: "append")
  • Rich optimistic templates with avatars and styling
  • Form validation with contextual error messages
  • Layout-stable wrapper technique to prevent shifts during optimistic updates
💬 Interactive

Discussion

A
Alice 2:30 PM

This is really helpful, thanks for sharing!

B
Bob 2:45 PM

I had the same question. Great explanation!

Add a comment
💡 Try: "Leave the comment empty and click the button to see validation"

How This Works

1. Comment System Configuration

---
const { comments = [] } = Astro.props;

const optimisticConfig = {
  template: "#comment-optimistic-template",
  errorTemplate: "#comment-error-template",
  errorMode: "append",  // Errors appear below form
  delay: 3000,
  target: ".comments-list",
  swap: "beforeend"     // New comments append to list
};
---

2. Form with Input Interpolation

<form
  class="comment-input-wrapper"
  hx-post="/api/comments"
  hx-target="closest .comment-system"
  hx-swap="outerHTML"
  data-optimistic={JSON.stringify(optimisticConfig)}
>
  <textarea
    name="comment"
    class="w-full p-3 border border-gray-300 rounded-lg"
    rows="3"
    placeholder="Share your thoughts..."
    required
  ></textarea>
  
  <button type="submit" class="btn btn-primary">
    Post Comment
  </button>
</form>

3. Optimistic Template with ${textarea} Interpolation

<template id="comment-optimistic-template">
  <div class="comment hx-optimistic border-l-4 border-blue-400 bg-blue-50 p-4 mb-4">
    <div class="flex items-start gap-3">
      <div class="avatar placeholder">
        <div class="bg-blue-500 text-white rounded-full w-10 h-10">
          <span class="text-sm font-bold">Y</span>
        </div>
      </div>
      <div class="flex-1">
        <div class="flex items-center gap-2 mb-1">
          <span class="font-semibold text-blue-700">You</span>
          <span class="text-xs text-blue-600">posting...</span>
        </div>
        <!-- User's input appears here immediately -->
        <p class="text-gray-800">${textarea}</p>
      </div>
    </div>
  </div>
</template>

4. Error Template for Append Mode

<!-- Errors appear below the form with errorMode: "append" -->
<template id="comment-error-template">
  <div class="alert alert-error mt-2">
    <span class="text-sm">❌ ${error}</span>
  </div>
</template>

5. Layout-Stable Styling

/* Prevent layout shift during optimistic updates */
.comment-form {
  min-height: 200px;
  transition: all 0.3s ease-out;
}

/* Smooth animation for new comments */
.comment.hx-optimistic {
  animation: slideIn 0.3s ease-out;
  border-left-color: #3b82f6 !important;
  background-color: #dbeafe !important;
}

Inline Editing with Validation

Click the text below to edit it in place. Changes are saved on blur or when you press Enter. 20% chance of simulated errors to demonstrate error handling.

Features demonstrated:
  • Multiple triggers (blur and Enter key)
  • PATCH HTTP method for updates
  • Inline editing pattern (click to edit)
  • External templates + error handling
  • Layout-stable wrapper technique to prevent shifts during outerHTML swaps
✏️ Interactive

Try editing this text:

Click to edit this text

How This Works

1. Layout-Stable Wrapper

<!-- Wrapper with min-height prevents layout shifts -->
<div style="min-height: 140px;">
  <InlineEditor text="Click to edit this text" />
</div>

2. InlineEditor Component Configuration

---
const { text } = Astro.props;

const optimisticConfig = {
  snapshotContent: true,
  template: "#inline-edit-optimistic",
  errorTemplate: "#inline-edit-error",
  delay: 2000,
};
---

3. Click-to-Edit HTML Structure

<div
  class="inline cursor-pointer px-2 py-1 rounded-md border-2 border-dashed 
         border-gray-300 bg-blue-50 hover:border-blue-500"
  data-editable="true"
  onclick="handleEditableClick(event)"
>
  <span class="display-text">{text}</span>
  <input
    class="edit-input"
    style="display: none"
    type="text"
    name="text"
    value={text}
    hx-patch="/api/edit"
    hx-trigger="blur, keyup[key=='Enter'] consume"
    hx-target="closest div"
    hx-swap="outerHTML"
    hx-ext="optimistic"
    data-optimistic={JSON.stringify(optimisticConfig)}
  />
</div>

4. Optimistic and Error Templates

<!-- Optimistic template shows blue styling -->
<template id="inline-edit-optimistic">
  <span class="text-blue-600 font-semibold bg-blue-100">
    ${this.value}
  </span>
</template>

<!-- Error template shows error indicator -->
<template id="inline-edit-error">
  <span>
    ${this.value} <span class="text-red-600 ml-1">❌</span>
  </span>
</template>

Product Rating with Rich Data

Rate a product and see rich optimistic updates from attributes and content. 25% chance of simulated errors to demonstrate error handling.

Features demonstrated:
  • Active button state with pulse animation
  • Full section replacement
  • Template per rating
  • Persistent interactivity after save
  • Layout-stable wrapper technique to prevent shifts during outerHTML swaps
⭐ Interactive

Wireless Headphones Pro

4.2 (847 reviews)
Rate this product:

How This Works

1. Layout-Stable Wrapper

<!-- Wrapper with min-height prevents layout shifts -->
<div style="min-height: 360px;">
  <ProductRating productId="wireless-headphones-pro" />
</div>

2. Dynamic Template Generation

---
// Create optimistic config for each button
const createOptimisticConfig = (rating: number) => ({
  template: `#rating-optimistic-${rating}`,
  errorTemplate: "#rating-error-template",
  delay: 2000,
});
---

<!-- Generate a template for each rating (1-5) -->
{[1, 2, 3, 4, 5].map((rating) => (
  <template id={`rating-optimistic-${rating}`}>
    <!-- Template shows the selected rating as active -->
    ...
  </template>
))}

3. Rating Button Configuration

{[1, 2, 3, 4, 5].map((rating) => (
  <button
    class="btn btn-outline btn-sm hover:btn-warning"
    data-product-id={productId}
    data-rating={rating}
    hx-post="/api/rating"
    hx-target={`#rating-${productId}`}
    hx-swap="outerHTML"
    hx-vals={JSON.stringify({ rating, productId })}
    data-optimistic={JSON.stringify(createOptimisticConfig(rating))}
  >
    {rating}⭐
  </button>
))}

4. Optimistic Template Structure

<template id="rating-optimistic-3">
  <div class="rating-section hx-optimistic">
    <h5>Rate this product:</h5>
    <div class="flex gap-2">
      <!-- Button 3 is shown as active/selected -->
      <button class="btn btn-outline">1⭐</button>
      <button class="btn btn-outline">2⭐</button>
      <button class="btn btn-warning btn-active" disabled>3⭐</button>
      <button class="btn btn-outline">4⭐</button>
      <button class="btn btn-outline">5⭐</button>
    </div>
    <div class="alert alert-info mt-4">
      <span>Saving your 3 star rating...</span>
    </div>
  </div>
</template>

5. Active Button Styling

/* Active state with pulse animation */
.btn.btn-active {
  background-color: #fbbf24 !important;
  border-color: #f59e0b !important;
  transform: scale(1.1);
  box-shadow: 0 0 0 3px rgba(251, 191, 36, 0.3);
  animation: pulse 0.5s ease-in-out;
}

@keyframes pulse {
  0%, 100% { transform: scale(1.1); }
  50% { transform: scale(1.15); }
}

🔧 Developer Warnings Demo

Open your browser's Developer Console and click these buttons to see helpful warning messages for unsupported interpolation patterns.

Expected Console Output:
[hx-optimistic] Unresolved interpolation pattern: ${this.querySelector('.nonexistent')}
Supported patterns:
  ${this.value} - element value
  ${this.textContent} - element text content
  ${this.dataset.key} - data attribute
  ${data:key} - data attribute shorthand
  ${attr:name} - any attribute
  ${Math.max(0, count)} - arithmetic with count
  ${status}, ${statusText}, ${error} - error context
See documentation for details.