---
title: "System & states"
description: "Empty, error, loading, stale and confirm states. They decide whether a product feels finished. Specs for 9 React components: EmptyState, ErrorState, Skeleton, StaleIndicator and 5 more."
canonical: "https://lopifi.com/docs/catalog/system-states"
---

# System & states

> Empty, error, loading, stale and confirm states. They decide whether a product feels finished. Specs for 9 React components: EmptyState, ErrorState, Skeleton, StaleIndicator and 5 more.

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

---

# System & states

Empty, error, loading, stale and confirm states. They decide whether a product feels finished.

---

## 82. `EmptyState`

Three variants, and each one is really its own component:

| Variant | Illustration | Content |
|---|---|---|
| `first-run` | Isometric spot, 64px, left of the copy | "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 what happened and wants the fix.

**Always exactly one next action.** An empty state with no action is a dead end.

`description` is one line, because an empty state is read at a glance. The particulars (which three filters are active, what the import does before anything posts) go in `detail`, behind the ⓘ on the title.

Illustration entrance: back-to-front, 60ms stagger, 8px translate, 320ms `--ease-out`, total capped at 480ms, **never loops**. [Spec](https://lopifi.com/md/docs/foundations/imagery-and-isometric).

## 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 is the most important line, and the only prose that stays on screen.** In a financial product, an error makes people worry about their money. Saying plainly that nothing happened to it is worth more than the retry button or a paragraph about the outage.

The `detail` (which endpoint, which timeout, which missing rate) belongs to whoever asks for it, so it rides an `InfoTip` on the title instead of a third paragraph. It's an `InfoTip`, not a `Tooltip`, because it opens on press and works on touch.

Variants: network · permission · not-found · server · stale-data. Each says what happened, what it means for the user's money, and what to do next.

## 84. `Skeleton`
Matches the exact shape, position and radii of the content it replaces. A skeleton that doesn't match makes the page jump when content lands, which is worse than no skeleton.

A slow breathe (`opacity 0.6↔1`, 1.6s) is allowed. **No shimmer sweep**: a gradient sliding across a full-page skeleton is expensive paint and the most obvious template tell there is.

## 85. `StaleIndicator`
"Last synced 12m ago · Retry". Appears on any surface showing cached or offline data. Dims the affected values to 0.7 — wrap them in `StaleValue`, or pass `stale` to the `Card` that holds them.

The age stays relative past the hour ("3h ago", "2d ago") — unlike `AsOf`, because the age is the alarm — with the absolute stamp and its named zone on hover. The server and first paint render the date alone; the age swaps in once the client clock is available, off the shared one-interval clock.

**Stale financial data shown as current is the most dangerous state in the product.** This component makes it 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.** It's used hundreds of times a day, which makes it the canonical Tier 0 component and the clearest case 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 required**: `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 is an accessibility failure, so hold-to-confirm is never the only path.

## 90. `DensityToggle`
compact / default / comfortable. Persisted per user and synced across devices. **Clamped to `comfortable` on phone** whatever the stored preference, because 32px rows with 13px text fail both touch targets and arm's-length legibility.

Sets `data-density` on a container; every list component reads it.
