Interaction feedback
Tell users what happened. Every view that depends on data or an async action must communicate its state — loading, empty, error, success — and confirm the result of every interaction. Never leave a dead state where the UI silently does nothing.
When to use
Use this skill for requests such as:
- "Add loading and error states" / "add a skeleton loader" / "add a spinner"
- "Show a success message" / "add a toast / notification"
- "Inline form validation" / "show validation errors"
- "Optimistic UI" / "update immediately and roll back on failure"
- "Empty state for this list"
- "This button gives no feedback when clicked"
The core model: handle every state
Any component backed by data or an async action must explicitly handle four states, plus partial/optimistic transitions:
- Empty — no data yet (first run, cleared filters, new account).
- Loading — request in flight.
- Error — the request or action failed.
- Success / ideal — data is present, action confirmed.
Treat "no design for this state" as a bug. A blank screen during loading, a list that shows nothing with no explanation, or a button that does nothing on click are all dead states.
Patterns
Loading
- Skeletons for content whose layout is known (cards, tables, profiles) — they preserve layout and reduce perceived wait.
- Spinners for short, indeterminate waits where layout is unknown.
- Inline button-busy (disabled +
aria-busy + label like "Saving…") for in-place actions; keep the button's size stable to avoid layout shift.
- Delay spinners ~200–300ms so fast responses don't flash a spinner. Reserve space so content doesn't jump when it arrives.
Success
- Prefer in-context confirmation (the row updates, a checkmark appears, the field shows saved) over a detached message.
- Use a toast for transient, low-importance confirmations ("Copied", "Saved").
- Keep it brief and don't block the next action.
Error
- Place errors near the cause (inline at the field/section), not only in a global banner.
- Make them actionable and recoverable: say what went wrong and offer a next step (Retry, Edit, Contact support). Never a dead-end.
- Preserve the user's input on failure.
Empty
- Explain the value of the area and offer a primary action ("No projects yet — create your first project"). Don't just print "No data".
- Distinguish first-use empty from filtered/cleared empty (offer "Clear filters").
Optimistic UI
- Update the UI immediately on the user's action, then reconcile with the server.
- On failure, roll back to the prior state and show a clear, recoverable message ("Couldn't save — changes reverted. Retry?").
- Use only where the action is very likely to succeed and rollback is cheap and clear.
Toasts / status messages
- Announce via
aria-live: polite for routine status, assertive / role="alert" for errors and urgent messages.
- Auto-dismiss neutral toasts (~4–6s); don't auto-dismiss errors the user must read or act on.
- Never put a critical or only action inside a toast — toasts disappear and aren't reliably reachable by keyboard/screen-reader users.
Micro-interactions
- Provide
:hover, :active, and :focus-visible feedback on interactive elements.
- Tie transitions/animation to motion tokens (duration, easing).
- Always gate non-essential motion behind
@media (prefers-reduced-motion: reduce) — disable or reduce animation, never make it required to understand state.
Accessibility of feedback
- Announce changes screen readers can't otherwise perceive: wrap status text in
role="status" / aria-live="polite", and errors in role="alert" / aria-live="assertive". Live regions must exist in the DOM before their content updates.
- Move focus on error to the first invalid field (or an error summary linking to fields) on submit, so keyboard users land at the problem.
- Don't rely on color alone — pair red/green with an icon and text.
- Disabled vs busy: use
aria-busy="true" for in-progress controls rather than fully removing them from the tab order; use disabled only when the control genuinely cannot be used.
- Mark invalid fields with
aria-invalid="true" and associate the message via aria-describedby.
Adapt to the stack
Read ../ux-foundations/references/stack-detection.md and run that procedure before writing code; report the detected framework, styling, and language in one line. With no project, fall back to the framework-agnostic pattern in examples/toast-aria-live.html.
Reuse what exists. Before adding dependencies, check for existing toast/notification libraries (e.g. react-hot-toast, sonner, vue-toastification, svelte-french-toast), form/validation libraries (react-hook-form, zod, vee-validate, formik), and state/data libraries (@tanstack/react-query, swr, redux-toolkit, Pinia) — they often already model loading/error/success states. Wire feedback into those rather than introducing parallel systems. Style via design tokens, not hardcoded values.
References and examples
references/states-and-feedback.md — decision table (situation → pattern → a11y mechanism → timing), the four states in depth, toast accessibility, validation timing, skeleton vs spinner, optimistic rollback UX, reduced-motion, and an anti-patterns list.
examples/toast-aria-live.html — working framework-agnostic accessible toast system (polite status region + assertive alert region, pause-on-hover, reduced-motion aware).
1---2name: interaction-feedback3description: This skill should be used when the user wants clear visual feedback for async actions and state changes — triggers like "add loading and error states", "show a success message", "add a toast/notification", "inline form validation", "optimistic UI", "empty state for this list", "this button gives no feedback", or "add a skeleton loader". It implements loading/empty/error/success states, toasts, micro-interactions, and their accessibility announcements, adapted to the detected stack.4---56# Interaction feedback78Tell users what happened. Every view that depends on data or an async action must communicate its state — loading, empty, error, success — and confirm the result of every interaction. Never leave a dead state where the UI silently does nothing.910## When to use1112Use this skill for requests such as:1314- "Add loading and error states" / "add a skeleton loader" / "add a spinner"15- "Show a success message" / "add a toast / notification"16- "Inline form validation" / "show validation errors"17- "Optimistic UI" / "update immediately and roll back on failure"18- "Empty state for this list"19- "This button gives no feedback when clicked"2021## The core model: handle every state2223Any component backed by data or an async action must explicitly handle four states, plus partial/optimistic transitions:24251. **Empty** — no data yet (first run, cleared filters, new account).262. **Loading** — request in flight.273. **Error** — the request or action failed.284. **Success / ideal** — data is present, action confirmed.2930Treat "no design for this state" as a bug. A blank screen during loading, a list that shows nothing with no explanation, or a button that does nothing on click are all dead states.3132## Patterns33341. **Loading**35 - **Skeletons** for content whose layout is known (cards, tables, profiles) — they preserve layout and reduce perceived wait.36 - **Spinners** for short, indeterminate waits where layout is unknown.37 - **Inline button-busy** (disabled + `aria-busy` + label like "Saving…") for in-place actions; keep the button's size stable to avoid layout shift.38 - **Delay spinners ~200–300ms** so fast responses don't flash a spinner. Reserve space so content doesn't jump when it arrives.39402. **Success**41 - Prefer **in-context confirmation** (the row updates, a checkmark appears, the field shows saved) over a detached message.42 - Use a **toast** for transient, low-importance confirmations ("Copied", "Saved").43 - Keep it brief and don't block the next action.44453. **Error**46 - Place errors **near the cause** (inline at the field/section), not only in a global banner.47 - Make them **actionable and recoverable**: say what went wrong and offer a next step (Retry, Edit, Contact support). Never a dead-end.48 - Preserve the user's input on failure.49504. **Empty**51 - Explain the **value** of the area and offer a **primary action** ("No projects yet — create your first project"). Don't just print "No data".52 - Distinguish first-use empty from filtered/cleared empty (offer "Clear filters").53545. **Optimistic UI**55 - Update the UI **immediately** on the user's action, then reconcile with the server.56 - On failure, **roll back** to the prior state and show a clear, recoverable message ("Couldn't save — changes reverted. Retry?").57 - Use only where the action is very likely to succeed and rollback is cheap and clear.58596. **Toasts / status messages**60 - Announce via `aria-live`: **polite** for routine status, **assertive** / `role="alert"` for errors and urgent messages.61 - Auto-dismiss neutral toasts (~4–6s); **don't auto-dismiss errors** the user must read or act on.62 - Never put a **critical or only** action inside a toast — toasts disappear and aren't reliably reachable by keyboard/screen-reader users.63647. **Micro-interactions**65 - Provide `:hover`, `:active`, and `:focus-visible` feedback on interactive elements.66 - Tie transitions/animation to **motion tokens** (duration, easing).67 - **Always gate non-essential motion** behind `@media (prefers-reduced-motion: reduce)` — disable or reduce animation, never make it required to understand state.6869## Accessibility of feedback7071- **Announce changes** screen readers can't otherwise perceive: wrap status text in `role="status"` / `aria-live="polite"`, and errors in `role="alert"` / `aria-live="assertive"`. Live regions must exist in the DOM **before** their content updates.72- **Move focus on error** to the first invalid field (or an error summary linking to fields) on submit, so keyboard users land at the problem.73- **Don't rely on color alone** — pair red/green with an icon and text.74- **Disabled vs busy**: use `aria-busy="true"` for in-progress controls rather than fully removing them from the tab order; use `disabled` only when the control genuinely cannot be used.75- Mark invalid fields with `aria-invalid="true"` and associate the message via `aria-describedby`.7677## Adapt to the stack7879Read `../ux-foundations/references/stack-detection.md` and run that procedure before writing code; report the detected `framework`, `styling`, and `language` in one line. With no project, fall back to the framework-agnostic pattern in `examples/toast-aria-live.html`.8081**Reuse what exists.** Before adding dependencies, check for existing toast/notification libraries (e.g. `react-hot-toast`, `sonner`, `vue-toastification`, `svelte-french-toast`), form/validation libraries (`react-hook-form`, `zod`, `vee-validate`, `formik`), and state/data libraries (`@tanstack/react-query`, `swr`, `redux-toolkit`, Pinia) — they often already model loading/error/success states. Wire feedback into those rather than introducing parallel systems. Style via design tokens, not hardcoded values.8283## References and examples8485- `references/states-and-feedback.md` — decision table (situation → pattern → a11y mechanism → timing), the four states in depth, toast accessibility, validation timing, skeleton vs spinner, optimistic rollback UX, reduced-motion, and an anti-patterns list.86- `examples/toast-aria-live.html` — working framework-agnostic accessible toast system (polite status region + assertive alert region, pause-on-hover, reduced-motion aware).