Feedback states
Every action that takes time has the same few states, and most apps only design the happy one. Your
job is to take one feature, find every state it can be in, and make each one clear: what the user
sees, how it moves, what they feel, and what it says. The
library supplies real references.
Work on one feature or screen at a time: a save, a send, a purchase, a search, or how one screen
loads its data. If the user did not name one, ask which. Do not change code until step 5.
1. Map the triggers and the states from the code
Read the feature's view and the code that does the work (the view model, the
function, the
network call), plus any shared loading, error or freshness views and modifiers the app already has.
First list what starts the work, and every place it can start from (a button and a cell can both
add the same thing, and two entry points are how races happen). A user action (a tap on Save) is
one trigger. A screen load often
has several: opening the screen, coming back to the app, a timer, changing a filter, pull to refresh,
a Refresh or Try again button. Feedback differs by trigger, so keep them apart.
Then list every state the feature can actually reach, not just the ones it draws today:
- Working, nothing to show yet: the first load, or a user action in progress. Note how long it
can take: under a second, a few seconds, or longer.
- Working, saved data on screen: showing what was there before while fresh data loads. For any
data screen this is the most important state.
- Success: it worked. Is there a next step, or does the user stay put?
- Partial success: the main thing worked but a side effect failed (saved, but not exported).
- Empty: it worked but there is nothing to show.
- Error: it failed. Which errors are possible (offline, server, validation, permission), and is
saved data still on screen?
- Retry: what happens when they try again, and when that fails too.
- Interrupted: they leave, background the app, change the filter, or tap again while it works.
States inside a system screen you cannot restyle (the photo picker, the camera, a share sheet) are
out of scope: design what happens after it closes.
If the action replaces or deletes something, check in the code that the old data survives a failure
(is it removed before the new data is safely saved?). Losing data is worse than any missing state.
For each, note the file and line and what the user sees today. Note feedback code that can never run
(an overlay inside a sheet that is already closed) so it can be removed. Also check that every promise in the
words is true in the code: "we will refresh when you are back online" needs something that watches
the network. If you cannot tell a state's look without running the app, say "not verified".
If a state is already handled well, mark it Keep and move on. The report is about the gaps.
2. Decide each state
Keep the whole flow calm and quick. Rules that hold almost everywhere:
- Haptics answer a touch. Success, error and confirmation haptics only when the result answers
something the user just did (Save, Refresh, Try again), even if the work took a few seconds. Never
for a load that started on its own (opening the screen, a timer, coming back to the app), and never
two haptics for one event: search every haptic call the success path reaches, across files, since
a view and a cell often each add one.
- Working: under about 400 ms, show no spinner (a flash reads as a glitch), but a load the user
asked for still needs a small sign that it happened. Longer, show progress in place, on the thing
being worked on, not a full screen blocker. After about 3 seconds, say what is happening.
- Saved data on screen: keep it visible and readable while fresh data loads, with a quiet sign
that it is updating. Never blank a screen that had data.
- Success: confirm in place, on the control the user touched, then get out of the way.
- Empty: say why it is empty and what to do next. A still illustration is fine; a looping one is
usually noise.
- Error: keep what the user typed or chose, and keep saved data on screen. Say what went wrong in
plain words and what to do. If it answers a tap, an error haptic and a small, brief shake on that
control are enough (about 6 points, 3 swings, 0.3 seconds). If nobody tapped, no haptic and no shake: a calm notice is enough.
- Partial success: confirm what worked, then a quiet note about what did not. Never an alert
titled as a failure for something that mostly worked.
- Retry: if there is no way to retry (an alert that only says "try again"), add one, and keep the
user's input in memory so retrying does not mean starting over. The button shows a working state
while it tries. A second failure must look different
from the first ("Still offline. Tried at 14:05."), or it looks like the tap did nothing.
- Interrupted: never leave a spinner running when the work has stopped, and never let a late
result overwrite a newer one.
Match the app's tone and reuse its own shared views and modifiers before proposing new ones. Every
motion needs a Reduce Motion version (a crossfade, or no motion).
3. Find references
Search only for the states you are changing that need motion, usually success and retry, sometimes
working. Skip states marked Keep and states with no motion.
Call
with the motion, the moment and the tone in plain words ("calm in-place
check after saving", "button shows a small spinner while it retries"). Do not lead with a surface
name like widget or settings. Judge results yourself by title, summary,
and
; the
scores barely separate good from poor matches. The library is strong on celebrations and button
feedback and thin on calm errors, retries, offline notices and loading skeletons: for those, search once at most,
and expect "No close match" plus the motion described in words to be the answer.
4. Report
Lead with the gaps in one to three sentences, worst first (a wrong haptic or a broken promise beats
a missing animation). Then one table, in the order the user meets the states:
| State | Where | Today | What to do | Haptic | Words | Shot |
|---|
Use Keep for states that are already right, and "Not needed" in the Shot column for states with
no motion. Reuse the app's own error messages where it already maps errors to words. Below the table,
list any data loss risk first, then dead feedback code to remove. Then the Reduce Motion version in one line, and ask whether to build it.
5. Build it, when asked
Model the states explicitly (an enum with the cases you listed) rather than a set of booleans, so
impossible combinations cannot happen. Fetch
60fps_get_motion_breakdown
and
only for the shots the user picked, and adapt their timing, not their look.
Keep the user's design system. Make sure it compiles.
Write plainly: short sentences, common words. Never use em dashes or en dashes.
Not for
Android, Flutter, React Native or web. Where an app should animate in general: use
find-animation-opportunities
.