# System & states

> The states that decide whether a product feels finished. Specs for 9 React components: EmptyState, ErrorState, Skeleton, StaleIndicator and 5 more.

Source: https://lopifi.com/docs/catalog/08-system-states
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

# 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](../foundations/08-imagery-and-isometric.md).

## 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.
