UX States Spec - every screen has four states, not one
Who fills this in: UI/UX (fast, so devs can build against it). Why: the “happy path with data” is the easy 20%. Loading, empty, and error are where real users live. Specifying all four before the build is Way #1 for the front end - and it’s how you avoid a demo that white-screens when the API hiccups.
Fill the state tables for each endpoint, make the data-viz call, then the accessibility note. Delete the quote-blocks before handing it over.
The four states - define each, every time
For each consumer screen, specify what the user sees in Loading, Populated, Empty, and Error. “Empty” and “Error” are different: empty = the call succeeded but there’s nothing to show; error = the call failed (renders from the Problem envelope, REQ-API-5).
Screen A - Compare (GET /api/compare)
| State | Trigger | What the user sees |
|---|---|---|
| Loading | Request in flight | Skeleton radar + skeleton bars; no layout shift when data lands |
| Populated | 200 with user[] + comparison[] | Radar overlay: the user’s scores vs the scope average (see decision below) |
| Empty | 200 but every dimension is responded:false | Friendly “No comparison yet - complete your assessment to see how you stack up”, not a blank chart |
| Error | non-2xx / Problem body | Inline error card showing Problem.title; a Retry action; the rest of the dashboard stays usable |
Edge case to spec explicitly: Alice has two N/A dimensions (governance, business_impact =
0.0,responded:false). Decide how the radar renders those - a visible gap/dashed segment beats plotting a misleading0.
Screen B - Recommend (GET /api/recommend)
| State | Trigger | What the user sees |
|---|---|---|
| Loading | Request in flight | 5 placeholder list rows |
| Populated | 200 with up to 5 recommendations | Ranked list: title + one-line reason + gap indicator (see decision below) |
| Empty | 200 with [] (e.g. user completed everything relevant) | “You’re all caught up on your weak spots” - a win, framed as one |
| Error | non-2xx / Problem body | Inline error row + Retry; never silently show an empty rail on failure |
Data-visualisation decision (record the why - Way #5)
| Screen | Chosen viz | Why | Rejected |
|---|---|---|---|
| Compare | Radar overlay - two series on one radar (you vs scope average) | One shape, instant “am I inside or outside the team?” read across all 6 dimensions at once | Side-by-side bars (harder to compare 6 dims at a glance) |
| Recommend | Ranked list - each row: lesson title + reason + a gap indicator (bar/pip sized to gapScore) | Order is the message; the gap indicator shows why this lesson ranks where it does, tying the UI back to the contract’s gapScore | A chart (recommendations are a to-do list, not a trend) |
Capture this in a one-line ADR if it was contested - that’s where the trade-off reasoning lives (
adr-template.md).
Accessibility note (non-optional - QUAL-PRINC-SDLC)
- Don’t rely on colour alone. The compare overlay must distinguish “you” from “average” by shape/pattern/label, not just hue - and the recommend gap indicator needs a text/numeric value beside the bar.
- Contrast & text: meet WCAG AA contrast; never hard-code an unreadable colour pair for the two radar series.
- Screen readers: charts need a text alternative - e.g. the populated radar exposes the same numbers as a visually-hidden table; each recommendation row reads as “[title], gap [n], because [reason]”.
- Keyboard & focus: Retry actions and any list interactions are reachable and operable by keyboard; visible focus states.
- Motion: skeleton/loading shimmer respects
prefers-reduced-motion.
Hand this to your devs alongside the contract in ../../../../stacks/nextjs/openapi.yaml. The five ways: ways-of-working.md.
Downloads for this session
Grab the templates and sample files used here.