Swaps & FX
The rule for this 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 and 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. 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 looks derived:
--text-secondaryuntil it's focused. - 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.
- A pair with no quote has no rate. Never a 1:1 fallback: the receive side stays empty, the breakdown is withheld, and the line says "No quote for USD to JPY."
- The fee is in the send currency.
feetakes oneMoneyValueper currency the user can send from; after a flip, a send currency with no fee listed is unquoted rather than charged another currency's number. Omitted, no fee is charged. - The balance check runs only when the send side is in the balance's currency. After a flip into another currency the balance and Max stand down.
defaultFrom,defaultToanddefaultAmountset the opening state.
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 linear rule-line progress bar, matching the statement grammar, not 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. 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, with 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, so the library reads as 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. The 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, so the axis is labeled clearly.
Draws once on mount. Instant on period change.
43. RateTicker#
Live multi-pair ticker. The value flash is the point. Pauses off-screen and on tab hide. Never a marquee: a scrolling ticker is continuous large-surface motion that can't be read or 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 isn't enough to decide on.
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 of the isometric flow-diagram treatment, where motion explains the mechanism.
46. FxExposure#
Position by currency with unrealized gain/loss. Uses polarity="standard" per position, and keeps realized and unrealized separate. They're different facts, and mixing them misstates the position.