PMaker home
When the user clicks, something on screen must change; without change, their default assumption is that they missedInstant feedbackAppears the moment of the click, answering 'did I click?'. Button color change, loading stateResult feedbackAppears when the operation completes, answering 'did it succeed?'. Toast, error messageState feedbackPersists after the operation, answering 'what's the state now?'. The data actually changed, the flag flippedShow 'saved' while the list still shows old data, and users refresh the page to check

The button changes immediately, the result is announced, and the data actually updates. Miss any layer and users wonder whether they clicked at all.

Every Action Needs Feedback

After the click, how does the user know it worked?

When a user clicks, something on screen must change. Without change, their default assumption is that they missed. The principle goes back to Don Norman's The Design of Everyday Things: after an action, the system must respond visibly within a reasonable time, or users can't connect "what I did" with "what happened." norman-wiki

What you'll run into:

  • Users click submit three times and the backend receives three duplicate rows
  • The click registers two seconds later, and in between they assume it's broken
  • The operation succeeded, but nothing on the page looks any different

Three layers of feedback

Every operation needs three layers, each answering a different question:

Layer When it appears The question it answers
Instant The moment the finger lifts Did I click? Button changes color, enters loading state, ripple effect
Result When the operation completes Did it succeed? Toast, status tag, error message
State Persists after the operation What's the state now? The list data really changed, the marker flipped

The third layer is the one people skip. Show a "saved successfully" toast while the list still holds old data, and users doubt whether it actually stored—then refresh the page to check.

The three layers aren't optional. Instant feedback answers "did I click," result feedback answers "did it succeed," and state feedback answers "what's the state now." Every layer you skip becomes an extra action your users have to take on their own: clicking submit three times, refreshing to verify, or navigating elsewhere to see if the data changed.

Pick the form by duration

When to give feedback depends on how long the operation takes:

  • Under 0.1s, show no loading state; just present the result.
  • 0.1 to 1s, a local loading state inside the button. Don't cover the whole page.
  • Over 1s, a skeleton screen or progress bar. If the duration is unknown, the bar must at least move—a frozen progress bar is more anxiety-inducing than none at all.
  • Over 10s, let it run in the background and offer an exit, rather than making users wait.

This breakdown also explains why "loading" shouldn't be slapped on randomly. For a save that finishes in 0.3 seconds, forcing a spinner makes it feel slower. Timing is itself a design decision, not decoration.

Optimistic updates

One class of operations can skip waiting for the server: likes, favorites, checking off a todo. Update the UI as if it succeeded, and roll back with a message if the request fails.

  • Good fits: lightweight, high-frequency actions where rolling back costs nothing. Favoriting by mistake and un-favoriting causes no loss.
  • Bad fits: anything involving money, irreversibility, or results computed server-side. Orders, payments, and deletions must wait for the real outcome.
  • On failure, roll back and explain. Silently reverting is worse than never being optimistic—users think it worked, then discover five minutes later that it didn't.

One more judgment worth keeping: optimistic updates solve a latency problem, not a failure problem. If the operation fails often, fix the failure rate before you talk about optimism—doing optimistic updates on an unstable API doubles down on the uncertainty you're handing to users.

Note for the AI

Keep this in the project's CLAUDE.md:

## Operation feedback

Every operation that triggers a request must implement three layers:
1. Instant: the button enters a loading state and is disabled the
   moment it's clicked, preventing duplicate submissions.
2. Result: success or failure both need an explicit message.
   Failure messages include the reason and the next step
   (see the copy guidelines).
3. State: after the operation, the related data on the page must
   refresh. Showing a toast without updating the list is not enough.

Choose the form by duration:
- < 0.1s: no loading state
- 0.1~1s: local loading state inside the button, no full-page overlay
- > 1s: skeleton screen or progress bar
- > 10s: allow background running and provide a way to leave

Optimistic updates are only for lightweight actions like likes,
favorites, and checkboxes, and they must roll back with a message
on failure. Anything involving money, deletion, or irreversible
operations always waits for the server result.

This turns "three layers of feedback" from a verbal requirement into a default for every generated operation—especially the line about refreshing the list, which targets the fake success that looks good but never actually saved.

References

  1. The Design of Everyday Things — Wikipedia