Skip to content

Optimistic UI

When to Use

Any action where the expected server response is success and failure is rare — liking a post, toggling a setting, reordering items, adding a tag. The UI updates instantly; the server confirms in the background. When the server fails, the UI rolls back with a non-blocking error.

Optimistic UI is about perceived performance. Users feel the app is fast because the feedback is immediate, not because the network is faster.

Decision

Scenario Use Optimistic UI? Why
Toggle like/bookmark/follow Yes Failure rate < 1%; rollback is non-destructive
Reorder list items Yes User sees intent immediately; rollback is a re-sort
Add comment/post Yes with caution Show optimistic state, replace with server-returned item after confirm
Delete a record Maybe — with confirmation Rollback is confusing after deletion; consider "undo" toast instead
Payment/financial transaction No Consequences of incorrect optimistic state are serious
Destructive irreversible action No Users must wait for confirmation before UI reflects it
File upload No Progress indication is more honest than optimistic completion

Pattern: Optimistic Toggle

async function optimisticToggle(element, action) {
  const previous = element.dataset.state;          // Store rollback value
  const next = previous === 'liked' ? 'unliked' : 'liked';

  // 1. Update UI immediately
  element.dataset.state = next;
  element.setAttribute('aria-pressed', next === 'liked');

  try {
    // 2. Sync with server async
    const result = await action(next);
    // 3. Replace optimistic state with server's canonical response
    element.dataset.state = result.state;
  } catch {
    // 4. Rollback on failure
    element.dataset.state = previous;
    element.setAttribute('aria-pressed', previous === 'liked');
    showErrorToast('Could not save — please try again');
  }
}

Pattern: Optimistic List Item Add

async function optimisticAdd(list, createFn, itemData) {
  const tempId = `temp-${Date.now()}`;     // Temporary ID for DOM reference
  const tempItem = renderItem({ ...itemData, id: tempId, pending: true });
  list.appendChild(tempItem);               // Show immediately

  try {
    const saved = await createFn(itemData);
    const realItem = renderItem(saved);
    list.replaceChild(realItem, tempItem);  // Replace temp with server response
  } catch {
    tempItem.remove();                      // Rollback — remove temp item
    showErrorToast('Could not save — check your connection');
  }
}

Pending State Indicators

Approach When to Use Avoid When
Subtle opacity reduction (0.6) on pending items Normal optimistic adds High-frequency actions — constant dimming looks broken
Spinner inside button, button stays enabled Slow operations (>1s) Instant operations — spinner flash is worse than nothing
No indicator (full confidence) Toggle states with <100ms server response Slow connections — users retry a completed action
"Saving..." label near affected area Autosave patterns Inline editing — label steals visual attention

Rollback UX

Good rollback is invisible: the state snaps back, and a non-blocking toast explains what happened. Never use a blocking modal for rollback — the user has already moved on.

The toast should: - Be dismissible immediately (not wait 5 seconds to disappear) - Offer a retry action if the action had user intent - Not interrupt what the user is currently doing

Common Mistakes

  • No rollback implementation — server failure silently corrupts UI state
  • Rollback without error message — user has no idea the action failed
  • Blocking the UI while "confirming" optimistically — defeats the purpose; the whole point is non-blocking
  • Optimistic state for destructive actions — user thinks data is deleted; rollback is confusing
  • Replacing rollback with page reload — extreme; use rollback state + toast
  • Using a temporary ID and never replacing it — server returns the real ID; always reconcile

See Also