App interaction patterns
Interactivity is not a feature list, it is a set of promises: that the page will tell you what it is
doing, what it is showing, and how to get back. Most interactive dashboards fail on the second and
third.
The state machine — build this first
Every data surface has seven states. Designing three of them and discovering the rest in
production is the normal failure.
| State |
Looks like |
The mistake to avoid |
| Loading |
Skeleton in the shape of the result |
A spinner that hides the layout, so the page jumps when data lands |
| Ready |
The data |
— |
| Empty (genuine) |
"No transactions in this period" + what would produce some |
Rendering an empty chart frame, which reads as broken |
| Filtered to nothing |
"No results for these filters" + which filters + a reset |
Same blank panel as genuine empty — completely different meaning |
| Partial |
The data you have, plus what is missing and why |
Silently showing 8 of 12 segments as if that were all of them |
| Stale |
The data, plus its age, plus a retry |
Showing a stale figure as current |
| Error / no access |
What failed and what to do |
A blank region, or a raw error string |
Empty and filtered-to-nothing are different states. Conflating them is the most common
interaction bug in dashboards: the user cannot tell whether there is no data or whether they broke
the view.
Skeletons should match the final layout's dimensions so nothing shifts when data arrives.
Filtering
Show the active filter set, always. A page filtered to one segment that looks identical to the
unfiltered page will be misread, and someone will quote the number.
Showing: Enterprise · North America · FY26 Q3 [Reset all]
× × ×
Rules:
- Each active filter is individually removable, and there is a global reset.
- Show what a filter excluded: "1,204 of 14,882 rows". A count that shrinks silently hides the
scope change.
- Filters apply on change for cheap operations; for expensive ones, debounce ~300ms or add an
explicit Apply — but never leave the user unsure whether their change took effect.
- Preserve filter state across a re-render. Losing it on every interaction is how a page becomes
unusable.
- Disabled options should say why, or be removed. A greyed-out option with no explanation reads as a
bug.
Cross-filtering and linked views
Clicking a bar in one chart filters the others. Powerful, and easy to make confusing.
- Selection must be visibly persistent — the clicked element stays highlighted while its filter
is active. Otherwise the user cannot tell what caused the other charts to change.
- De-emphasise rather than remove the unselected data where it makes sense. Seeing the selection
in context beats seeing it alone.
- One selection model per page. Click-to-filter in one chart and click-to-drill in another, with
no visual distinction, is a trap.
- Always provide the escape: click the background, press Escape, or hit a visible Clear.
- Cross-filtering should never change a chart's scale silently — a bar that grows because the axis
rescaled, not because the value changed, is a misread.
Drill-down
Moving from summary to detail. The essentials:
- Show the path:
All accounts › 6200 Professional fees › July 2026. Every segment is clickable
to go back up.
- Keep the parent context visible — the figure you drilled from should remain on screen, so the
detail can be checked against it.
- Detail must reconcile to the summary. If the drill-down rows do not sum to the bar you clicked,
say why (exclusions, rounding, a different grain) rather than letting the reader find the gap.
- Browser back should feel natural even without URLs — Escape or a breadcrumb click is the
substitute.
Brushing and zoom
For time series with more points than pixels.
- Keep an overview strip showing the full range with the selection marked, so the user never
loses the whole.
- Show the selected range as text (
2026-03-01 → 2026-06-30), not just as a visual extent.
- Double-click resets. Make it discoverable with a Reset zoom control.
- Zooming changes the view, never the underlying aggregation, unless you say so — silently
switching from monthly to daily on zoom changes what the marks mean.
Tooltips and hover
The rule that matters most:
Hover reveals detail. It never reveals the primary value.
Hover is dead on touch, invisible in print, and unreachable by keyboard. If a number matters, it is
on the page.
Good tooltips: precise value at full precision, the comparison (vs prior, vs plan), the share of
total, the record count behind an aggregate. Bad tooltips: the label that is already on the axis.
Anchor near the cursor but flip near edges so the tooltip never leaves the viewport, and never
let it cover the mark it describes.
Selection and bulk action
For tables and lists:
- Selection count is always visible, with a clear-all.
- Shift-click selects a range; it is expected and cheap to add.
- Actions on a selection state what they will affect: "Export 42 rows", not "Export".
- Anything irreversible confirms, and the confirmation names the scope and the consequence.
Keyboard
An app that cannot be driven by keyboard is unfinished. Minimum:
| Key |
Action |
Tab / Shift+Tab |
Move between controls, with a visible focus ring |
Enter / Space |
Activate |
Escape |
Close, clear selection, exit drill-down |
| Arrows |
Move within a group (tabs, table cells, legend items) |
/ |
Focus search, where one exists |
Use one tab stop for a group of related controls plus arrow navigation inside it — otherwise a
20-item legend costs 20 tab presses to get past. See artifact-accessibility.
Responsiveness of feedback
| Delay |
Feedback needed |
| < 100ms |
None — feels instant |
| 100ms - 1s |
Subtle: cursor, control dims |
| 1s - 5s |
Skeleton or progress, and keep the page interactive |
| > 5s |
Progress with an estimate, and a cancel |
Never block the whole page for a partial update. Update the region that changed.
Related skills
live-data-artifacts — stale, error, and no-access states come from there
stateful-artifacts — read-only and conflict states
visual-hierarchy — controls must not outweigh results
microcopy-and-states — the words each state uses
artifact-performance — what makes interaction feel instant
artifact-accessibility — keyboard and focus in depth
1---2name: app-interaction-patterns3description: Interaction design for data apps - filtering, cross-filtering, drill-down, brushing, linked views, selection, and the complete state machine every data surface needs (loading, empty, partial, stale, error, no-access, filtered-to-nothing). Trigger on "filter", "drill down", "interactive dashboard", "click a bar", "loading state", "empty state", "error state", "linked charts", "cross-filter", "make it interactive".4---56# App interaction patterns78Interactivity is not a feature list, it is a set of promises: that the page will tell you what it is9doing, what it is showing, and how to get back. Most interactive dashboards fail on the second and10third.1112---1314## The state machine — build this first1516Every data surface has **seven** states. Designing three of them and discovering the rest in17production is the normal failure.1819| State | Looks like | The mistake to avoid |20|---|---|---|21| **Loading** | Skeleton in the shape of the result | A spinner that hides the layout, so the page jumps when data lands |22| **Ready** | The data | — |23| **Empty (genuine)** | "No transactions in this period" + what would produce some | Rendering an empty chart frame, which reads as broken |24| **Filtered to nothing** | "No results for these filters" + **which filters** + a reset | Same blank panel as genuine empty — completely different meaning |25| **Partial** | The data you have, plus what is missing and why | Silently showing 8 of 12 segments as if that were all of them |26| **Stale** | The data, plus its age, plus a retry | Showing a stale figure as current |27| **Error / no access** | What failed and what to do | A blank region, or a raw error string |2829**Empty and filtered-to-nothing are different states.** Conflating them is the most common30interaction bug in dashboards: the user cannot tell whether there is no data or whether they broke31the view.3233Skeletons should match the final layout's dimensions so nothing shifts when data arrives.3435---3637## Filtering3839**Show the active filter set, always.** A page filtered to one segment that looks identical to the40unfiltered page will be misread, and someone will quote the number.4142```43Showing: Enterprise · North America · FY26 Q3 [Reset all]44 × × ×45```4647Rules:48- Each active filter is individually removable, and there is a global reset.49- **Show what a filter excluded**: "1,204 of 14,882 rows". A count that shrinks silently hides the50 scope change.51- Filters apply on change for cheap operations; for expensive ones, debounce ~300ms or add an52 explicit Apply — but never leave the user unsure whether their change took effect.53- Preserve filter state across a re-render. Losing it on every interaction is how a page becomes54 unusable.55- Disabled options should say why, or be removed. A greyed-out option with no explanation reads as a56 bug.5758---5960## Cross-filtering and linked views6162Clicking a bar in one chart filters the others. Powerful, and easy to make confusing.6364- **Selection must be visibly persistent** — the clicked element stays highlighted while its filter65 is active. Otherwise the user cannot tell what caused the other charts to change.66- **De-emphasise rather than remove** the unselected data where it makes sense. Seeing the selection67 in context beats seeing it alone.68- **One selection model per page.** Click-to-filter in one chart and click-to-drill in another, with69 no visual distinction, is a trap.70- **Always provide the escape**: click the background, press Escape, or hit a visible Clear.71- Cross-filtering should never change a chart's *scale* silently — a bar that grows because the axis72 rescaled, not because the value changed, is a misread.7374---7576## Drill-down7778Moving from summary to detail. The essentials:7980- **Show the path**: `All accounts › 6200 Professional fees › July 2026`. Every segment is clickable81 to go back up.82- **Keep the parent context visible** — the figure you drilled from should remain on screen, so the83 detail can be checked against it.84- **Detail must reconcile to the summary.** If the drill-down rows do not sum to the bar you clicked,85 say why (exclusions, rounding, a different grain) rather than letting the reader find the gap.86- Browser back should feel natural even without URLs — Escape or a breadcrumb click is the87 substitute.8889---9091## Brushing and zoom9293For time series with more points than pixels.9495- Keep an **overview strip** showing the full range with the selection marked, so the user never96 loses the whole.97- Show the selected range as text (`2026-03-01 → 2026-06-30`), not just as a visual extent.98- **Double-click resets.** Make it discoverable with a Reset zoom control.99- Zooming changes the *view*, never the underlying aggregation, unless you say so — silently100 switching from monthly to daily on zoom changes what the marks mean.101102---103104## Tooltips and hover105106The rule that matters most:107108> **Hover reveals detail. It never reveals the primary value.**109110Hover is dead on touch, invisible in print, and unreachable by keyboard. If a number matters, it is111on the page.112113Good tooltips: precise value at full precision, the comparison (vs prior, vs plan), the share of114total, the record count behind an aggregate. Bad tooltips: the label that is already on the axis.115116Anchor near the cursor but **flip near edges** so the tooltip never leaves the viewport, and never117let it cover the mark it describes.118119---120121## Selection and bulk action122123For tables and lists:124125- Selection count is always visible, with a clear-all.126- Shift-click selects a range; it is expected and cheap to add.127- **Actions on a selection state what they will affect**: "Export 42 rows", not "Export".128- Anything irreversible confirms, and the confirmation names the scope and the consequence.129130---131132## Keyboard133134An app that cannot be driven by keyboard is unfinished. Minimum:135136| Key | Action |137|---|---|138| `Tab` / `Shift+Tab` | Move between controls, with a visible focus ring |139| `Enter` / `Space` | Activate |140| `Escape` | Close, clear selection, exit drill-down |141| Arrows | Move within a group (tabs, table cells, legend items) |142| `/` | Focus search, where one exists |143144Use one tab stop for a group of related controls plus arrow navigation inside it — otherwise a14520-item legend costs 20 tab presses to get past. See `artifact-accessibility`.146147---148149## Responsiveness of feedback150151| Delay | Feedback needed |152|---|---|153| < 100ms | None — feels instant |154| 100ms - 1s | Subtle: cursor, control dims |155| 1s - 5s | Skeleton or progress, and keep the page interactive |156| > 5s | Progress with an estimate, and a cancel |157158Never block the whole page for a partial update. Update the region that changed.159160---161162## Related skills163164- `live-data-artifacts` — stale, error, and no-access states come from there165- `stateful-artifacts` — read-only and conflict states166- `visual-hierarchy` — controls must not outweigh results167- `microcopy-and-states` — the words each state uses168- `artifact-performance` — what makes interaction feel instant169- `artifact-accessibility` — keyboard and focus in depth