System & states
The states that decide whether a product feels finished.
82. EmptyState
Three variants, and they are genuinely different components:
| Variant | Illustration | Content |
|---|---|---|
first-run |
Isometric spot, 160px | "No transactions yet" + explanation + one primary action |
filtered |
None | "No transactions match these filters" + "Clear filters" |
period |
None | "No transactions between 1–30 Sep 2026" + "Widen the period" |
An illustration on the filtered variant is condescending — the user knows exactly what happened and wants the fix, not a picture.
Always exactly one clear next action. An empty state with no action is a dead end.
description is one line. The particulars — which three filters are active, what the import actually does before anything posts — go in detail, behind the ⓘ on the title. An empty state is read at a glance.
Illustration entrance: back-to-front, 60ms stagger, 8px translate, 320ms --ease-out, total capped at 480ms, never loops. Spec.
83. ErrorState
Couldn't load transactions. ⓘ ← the technical account, one press away
No transactions were affected. ← the financial reassurance
[Try again] [Contact support]
The reassurance sentence is the most important line, and it is the only prose that stays on screen. In a financial product, an error makes users worry that something happened to their money. Saying explicitly that nothing did is worth more than the retry button — and worth more than the paragraph explaining the outage.
The detail — which endpoint, which timeout, which missing rate — is real and belongs to the person who asks for it, so it rides an InfoTip on the title rather than a third paragraph. An InfoTip and not a Tooltip: it opens on press, so the account of the failure survives touch.
Variants: network · permission · not-found · server · stale-data. Each states what happened, what it means for their money, and what to do.
84. Skeleton
Structural, matching the exact shape, position, and radii of the content it replaces. Slow breathe (opacity 0.6↔1, 1.6s) permitted; no shimmer sweep — a translating gradient across a full-page skeleton is large-surface paint animation and the clearest template tell there is.
A skeleton that doesn't match causes a jump when content lands, and that jump is worse than no skeleton at all.
85. StaleIndicator
"Last synced 12m ago · Retry". Appears on any surface showing cached or offline data. Dims the affected values to 0.7.
Stale financial data presented as current is the most dangerous state in the product. This component exists so that state is impossible to miss.
86. Toast
Sonner, configured to our tokens. 400ms ease enter, 200ms exit, transitions not keyframes, stack offsets −0.05 scale / −16px / −0.2 opacity, swipe-dismiss at 45px or velocity > 0.11. Auto-dismiss pauses on tab hide.
Never the only channel for a critical error. Toasts are timed and dismissible; a failed payment persists inline.
87. CommandPalette
Zero animation. Opens instantly. Used hundreds of times a day — this is the canonical Tier 0 component and the clearest demonstration of the frequency law.
Money-aware search: typing 1240 finds transactions with that amount; typing >1000 filters by magnitude; typing a date range parses it. Grouped results, recents first, full keyboard navigation.
88. Sheet
Vaul, configured. The primary overlay on phone — a sheet enters from the thumb zone; a centered dialog puts its actions in the middle of the screen.
320ms --ease-drawer enter, 225ms exit, 1:1 finger tracking, spring.gesture release, velocity-aware dismissal, boundary damping, pointer capture, multi-touch protection. Snap points for partial heights. overscroll-behavior: contain. Footer padded to max(24px, env(safe-area-inset-bottom)).
89. HoldToConfirm
clip-path: inset() fill, 1500ms linear press, 200ms --ease-out release, scale(0.97) on the button. Slow where the user is deciding, fast where the system is responding.
Accessible alternatives are mandatory: Space to hold with aria-live progress at 0/50/100%, Escape to abort, and a typed-confirmation fallback. Gating a financial action behind a motor-skill requirement is an accessibility failure, so hold-to-confirm is never the only path.
90. DensityToggle
compact / default / comfortable. Persisted per user, synced across devices, clamped to comfortable on phone regardless of the stored preference — 32px rows with 13px text fails both touch targets and arm's-length legibility.
Sets data-density on a container; every list component reads it.