--- name: designing-user-experience description: Use when designing how an interface behaves rather than how it looks — user flows, navigation and information architecture, forms and validation, loading/empty/error states, confirmation and undo for destructive actions, button and error message wording, feedback for slow operations, or mobile and touch behavior. Also use when a UI looks finished but breaks on bad input, slow networks, no data, or first use. --- # Designing User Experience ## Overview Visual design decides what an interface looks like when everything goes right. UX decides what happens the rest of the time — and the rest of the time is most of the time. **Core principle:** Design the states, not the screens. A "screen" is one state of a component; shipping only that one is why generated UI looks finished and breaks on contact with real data. **Companion skills:** `designing-frontend-interfaces` for visual craft, `building-accessible-interfaces` for access, `reviewing-interface-quality` to audit a finished UI. ## The Iron Law ``` NO COMPONENT IS DONE UNTIL ITS EMPTY, LOADING, ERROR, AND PARTIAL STATES EXIST ``` A list that only renders when it has items is not a list component — it is a demo. If the design has no answer for "what does a new user with no data see", the design is incomplete. ## Checklist Create a task for each item: 1. **Map the flow** — entry point → steps → success, and every branch off it 2. **Fill the state matrix** — for every component that touches data 3. **Design the failure paths** — what breaks, what the user sees, how they recover 4. **Write the copy** — buttons, errors, empty states, confirmations 5. **Set the feedback budget** — what happens at 100ms, 1s, 10s 6. **Handle the destructive and irreversible** — confirm, undo, or both 7. **Check touch and small viewport** — targets, reach, keyboard-open behavior 8. **Walk the flow as a first-time user** — with no data, no permissions, on a slow connection ## Step 1: Map the Flow Before designing screens, write the flow as text. It takes two minutes and exposes gaps no mockup will. ``` GOAL: Reviewer approves a pending expense Entry: Email link → deep link into the item (may not be authenticated) Step 1: Authenticate → already logged in? skip Step 2: View item + receipt Step 3: Approve | Reject | Request info Success: Confirmation + next pending item queued Exit: No items left → done state, not an empty table Branches: Not authorized for this item → explain who is, offer to forward Item already actioned by someone → show who and when, no error framing Receipt fails to load → approve still possible, receipt shows retry Offline mid-approve → queue and retry, tell them it is queued ``` **The branches are the design work.** Any flow whose branch list is empty has not been thought about yet. **Reduce steps before styling them.** Each step is a chance to abandon. Ask of every one: can this be defaulted, inferred, deferred to later, or removed? A form field that can be derived should not be asked. Worked flows for the patterns that recur — onboarding, search and filtering, multi-step wizards, undo, permissions and paywalls: [references/flow-patterns.md](references/flow-patterns.md). ## Step 2: The State Matrix Every component that displays data or accepts input has these states. Enumerate them explicitly; do not discover them in production. | State | Trigger | Requirement | |---|---|---| | **Empty — first use** | New user, nothing created yet | Explain the feature and give the primary action. Never "No data." | | **Empty — no results** | Filter or search matched nothing | Echo the query, offer to clear filters. Different from first-use empty. | | **Empty — cleared** | User completed everything | Acknowledge it. This is a success state, not an absence. | | **Loading — first** | No cached data | Skeleton matching real layout, delayed ~200ms | | **Loading — refresh** | Data exists, updating | Keep old data visible, subtle indicator. Never blank the screen. | | **Partial** | Some sources failed | Show what loaded, mark what did not, offer retry for that part | | **Error — recoverable** | Timeout, 5xx, network | What happened, what to do, a retry control | | **Error — permanent** | 404, deleted, revoked | Say it plainly, offer the nearest useful destination | | **Error — permission** | 403 | Who does have access, how to request it | | **Offline** | Connection lost | What still works, what is queued | | **Too much** | 10,000 rows | Pagination or virtualization, plus a count | | **Stale** | Cache older than threshold | Timestamp and a refresh control | | **Success** | Action completed | Confirm it, and say what changed | **The four empty states are genuinely different.** Collapsing them into one "No items found" is the most common UX shortcut in generated interfaces. ``` First use: "No expenses yet — add your first to start tracking." [Add expense] No results: "No expenses match 'q3 travel'." [Clear filters] Cleared: "All caught up. Nothing needs review." Error: "Couldn't load expenses. Check your connection." [Retry] ``` ## Step 3: Forms Forms are where most UX failures live because they are where users actually type. ### Structure - **One column.** Multi-column forms cause skipped fields and are ambiguous with keyboard order. Exceptions: genuinely paired fields (city/state, expiry/CVC). - **Labels above inputs**, always visible. Placeholder-as-label disappears on focus, fails at low contrast, and breaks autofill and screen readers. - **Group related fields** with real spacing (see the proximity rule in `designing-frontend-interfaces`). - **Mark whichever is rarer** — if most fields are required, mark the optional ones instead. Never mark nothing. - **Show requirements before typing**, not as an error afterwards. "8+ characters, one number" belongs under the field from the start. ### Validation Timing | When | Do | |---|---| | While typing, field never blurred | Nothing. Validating on keystroke 3 of an email is hostile. | | On blur, field non-empty | Validate this field | | While typing, **after** an error was shown | Re-validate live so the error clears as they fix it | | On submit | Validate all, focus the first invalid field, summarize at top if more than 2 | | Server rejects | Map errors back to specific fields; never only a banner | Errors appear **next to the field**, not only in a summary. A summary at the top is an addition for long forms, never a replacement. ### Input Types Getting these right is most of mobile form UX — the correct keyboard appears automatically. ```html ``` Use `inputmode="numeric"` rather than `type="number"` for codes, PINs, and card numbers — `type="number"` adds spinners, allows `e` and `-`, and silently drops leading zeros. **Never block paste.** Blocking paste on password or confirmation fields breaks password managers and helps no one. ### Data Handling - **Never clear the form on error.** Losing typed data is the single most infuriating form behavior. - **Preserve input across navigation** where the form is long — sessionStorage or server draft. - **Be liberal in what you accept.** Strip spaces from card numbers and phone numbers rather than rejecting them. Accept `+1 (555) 123-4567` and normalize it yourself. - **Disable submit only while submitting**, never as a validation gate. A permanently disabled button with no explanation gives the user nothing to act on. ## Step 4: Copy Interface copy is UX, not decoration. It is often the entire difference between a usable and an unusable state. ### Buttons Name the action, not the abstraction. | Weak | Strong | |---|---| | Submit | Send invitation | | OK | Delete 3 files | | Yes / No | Discard changes / Keep editing | | Continue | Review order | A confirmation dialog's buttons must be readable **without** the dialog text. "OK" and "Cancel" on a delete confirmation force the user to re-read the prompt to know which is destructive. ### Errors Three parts: what happened, why, what to do. Never expose a stack trace or a raw status code to a user. | Bad | Good | |---|---| | "Error: invalid input" | "Enter a date after today — this event has already passed." | | "Something went wrong" | "Couldn't save. Your connection dropped — we kept your changes. [Retry]" | | "Error 403" | "You don't have access to this project. Ask its owner, Dana Reyes, for access." | | "Invalid email" | "Emails need an @ — did you mean dana@example.com?" | Never blame the user. "You entered an invalid date" → "That date has already passed." ### Voice - Second person, active voice, present tense. - Short. Cut every sentence that does not change what the user does. - No exclamation marks in error states. - No jargon the user did not introduce — "authentication token expired" → "you've been signed out." - Sentence case for buttons and headings; Title Case reads as shouting in UI. ## Step 5: Feedback and Latency Every user action needs acknowledgment within 100ms, even if the result takes longer. | Elapsed | Requirement | |---|---| | 0-100ms | Feels instant. Show nothing extra. A flashing spinner is worse than none. | | 100ms-1s | Immediate local feedback — button enters a pressed/busy state | | 1-10s | Skeleton or determinate progress. Keep the layout stable. | | 10s+ | Real progress, an estimate, and a cancel control | **Optimistic updates** for actions that nearly always succeed (like, star, reorder): apply the change immediately, reconcile on response, and revert with a clear message on failure. Do not use them for anything with financial or destructive consequences. **Never move content under a pointer.** Content that shifts after load causes mis-clicks. Reserve space for anything that will arrive: images with `width`/`height`, ad slots, async badges. ## Step 6: Destructive and Irreversible Actions **Prefer undo over confirm.** A confirmation dialog interrupts every user to protect against a rare mistake; undo protects against the mistake without interrupting anyone. ``` Do: [Delete] → item removed → toast: "Message deleted. [Undo]" (8-10s) Instead of: [Delete] → "Are you sure?" → [Yes] [No] ``` **Confirm when undo is genuinely impossible** — sending an email, charging a card, permanently destroying data, anything affecting other people. A real confirmation dialog: - States the **specific** consequence with the count and name: "Delete 47 records from *Q3 Archive*? This cannot be undone." - Labels the buttons with the actions, never Yes/No. - Puts the destructive action in a destructive color **and** distinct wording. - Requires typing the resource name only when the action is catastrophic and irreversible. Reserve that friction; overusing it trains users to type past it. - Focuses the **safe** option by default. Never use `window.confirm()` in production UI — it is unstyleable, blocks the main thread, and reads poorly to screen readers. ## Step 7: Navigation and Information Architecture - **The user must always know where they are.** Current section indicated in nav; page title matching the destination they clicked. - **Breadcrumbs for hierarchies deeper than two levels.** - **Back must work.** Modals, filters, tabs, and steps should be URL-addressable so back and refresh behave. A filtered view that resets on refresh is broken. - **Deep links must survive auth.** Land on a login, then return to the originally requested resource — not the dashboard. - **Flat beats deep.** Seven items at one level are easier than three levels of three. Add a level only when a category genuinely has its own landing content. - **Search is a feature, not a fallback.** If the answer to "how do users find X" is "search", the IA needs work — but ship search anyway. ## Step 8: Touch and Small Viewports | Rule | Value | |---|---| | Minimum touch target | 44×44 CSS px (WCAG 2.2 SC 2.5.8) | | Spacing between targets | 8px minimum | | Primary actions | Bottom half of the screen — thumb reach | | Destructive actions | Away from primary actions, never adjacent | | Hover-only affordances | Forbidden. Touch has no hover. | | Fixed bottom bars | Account for the on-screen keyboard and safe areas | ```css .bottom-bar { padding-bottom: max(var(--space-s), env(safe-area-inset-bottom)); } body { min-height: 100dvh; } /* not 100vh — browser chrome makes it overflow */ ``` Every gesture needs a visible equivalent. Swipe-to-delete is fine as an accelerator; it cannot be the only way to delete. ## Self-Review - [ ] Every data component has all four empty states distinguished - [ ] Loading never blanks already-visible content - [ ] Every error says what to do next, and has a control to do it - [ ] No error text exposes a status code or stack trace - [ ] Forms never clear on error - [ ] Validation fires on blur, not on keystroke - [ ] Every input has the right `type`, `inputmode`, and `autocomplete` - [ ] Button labels name their action and are readable out of context - [ ] Destructive actions have undo, or a confirmation naming the specific consequence - [ ] Filters, tabs, and modals are reflected in the URL - [ ] Touch targets ≥ 44px, primary actions in thumb reach - [ ] Nothing is reachable by hover alone - [ ] Walked the whole flow with no data, no permission, and a throttled connection ## Common Mistakes **The happy path is the only path.** Everything renders beautifully with three seeded items and collapses with zero or ten thousand. **"No data" as an empty state.** It tells the user nothing and offers no action. Empty states are the highest-leverage onboarding surface in the product. **Spinner replacing the whole page on refresh.** The user had content; now they have nothing. Keep it and indicate the update. **Disabled submit with no explanation.** The user cannot tell what is missing. Enable it and validate on click, or state what is incomplete. **Confirmation dialogs everywhere.** Confirming everything trains users to dismiss without reading, which is worse than no confirmation. Undo where possible. **Error toasts that auto-dismiss.** A 4-second toast carrying the only description of a failure is a message the user will miss. Errors persist until dismissed; only successes auto-dismiss. **Modal-in-modal.** Almost always a sign the flow needs a page, not a layer. **Icon-only buttons without labels.** Recognizable icons are a short list: close, search, menu, plus. Everything else needs a visible label or, at minimum, a tooltip plus an accessible name.