Skip to content

Swaps & FX

The governing rule for this entire family:

A rate is a perishable fact. It never appears without a timestamp, and it is never transacted on without a lock.


38. SwapWidget

The signature component. From/to with the flip.

┌─ 24 ──────────────────────────────────┐
│  You send                              │  label
│  [1,000.00]              [USD ▾]       │  figure-lg · currency select
│  Balance $4,120.00              [Max]  │  caption
│                                        │
│         ⟨ ⇅ ⟩  ← flip button           │  centered, overlapping the divider
│                                        │
│  Recipient gets                        │
│  [918.42]                [EUR ▾]       │
│                                        │
│  1 USD = 0.91842 EUR · 12s  [locked]   │  RateLine
│  ├─ ConversionBreakdown ─────────────┤ │
└────────────────────────────────────────┘

The flip

The one place spring.playful is used.

icon      rotateY? no — rotate(180deg), spring.playful { duration: 0.45, bounce: 0.22 }
fields    cross-translate: top ↕ bottom, transform only, same spring
values    swap INSTANTLY. The amounts do not interpolate. (Money Law 1)

The fields move; the numbers don't animate. That distinction is the whole doctrine in one component — motion explains what happened structurally, and never renders a false value.

Under reduced motion: instant swap, no rotation.

Rules

  • Editing either field recomputes the other, with a 140ms blur-masked crossfade on the computed value — never a count-up.
  • The computed field is visibly derived — --text-secondary until it's the focused one.
  • Debounce rate fetches at 300ms; never show a stale computed value as current. During refetch, dim the computed value to 0.6.
  • Insufficient balance is an inline error under the send field, not a disabled button.
  • Fee and rate are always visible without expanding anything.

39. RateLine

<RateLine from="USD" to="EUR" rate={0.91842} asOf={t} />
// 1 USD = 0.91842 EUR · 12s ago

5 significant figures, tnum. The timestamp is not optional — the component has no variant without it.

Live variant flashes on update (value flash), throttled to one flash per 400ms.

40. RateLock

A locked rate with a countdown. The countdown is a rule-line progress bar, linear, consistent with the statement grammar rather than a rounded progress pill.

On expiry: the rate refreshes, the new rate crossfades in, and the confirm action is disabled until the user acknowledges the new rate. Silently executing at a rate the user didn't see is the worst failure this family can have.

Warning state at 20% remaining — amber, plus a text countdown, never color alone.

41. ConversionBreakdown

Wise's honesty pattern, as a component. Every line visible.

Amount to convert                    1,000.00 USD
Our fee                                 −4.50 USD   ← itemized, named
Amount we'll convert                   995.50 USD
                                      ──────────    ← single rule
Rate  1 USD = 0.91842 EUR  · 09:12 UTC
Recipient gets                         914.29 EUR
                                      ══════════    ← double rule
  • Every fee named and itemized. Never a lump "Fees".
  • The rate line carries its timestamp.
  • Uses Rule — the same grammar as the statements, which is what makes the library feel like one system.
  • Percentage-of-total splits use largest-remainder allocation, so the parts sum to the total exactly.

42. CurrencyPairChart

Rate history with a period selector. Y-axis does not start at zero — it's a rate, not a quantity, and forcing zero destroys the resolution. This is the one sanctioned exception to the zero-baseline rule, and the axis is labeled clearly so it can't mislead.

Draws once on mount. Instant on period change.

43. RateTicker

Live multi-pair ticker. The value flash is the whole point. Pauses off-screen and on tab hide. Never a marquee — a scrolling ticker is continuous large-surface motion that can't be read and can't be paused.

44. SlippageControl

Tolerance setting that always states the worst case in absolute money: "You'll receive at least 910.15 EUR". A percentage alone is not a decision-grade number.

45. SwapRoute

Multi-hop path (USD → USDC → EUR). Rendered as a horizontal flow, each hop with its own rate and fee. A rare, legitimate use for the isometric flow-diagram treatment — motion explains the mechanism.

46. FxExposure

Position by currency with unrealized gain/loss. Uses polarity="standard" per position, and clearly separates realized from unrealized — they're different facts and conflating them misstates the position.

The 9 components specified here.

Built against this document

The surfaces that implement what this argues — running, computing, or shaped by it.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.