Skip to content

Currency Swap

A converter card: what you pay over what you receive, a round button that flips them, the rate and the action. Crypto or plain money.

import { CurrencySwap, CurrencySwapAsset, CurrencySwapSubmit } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Give it the currencies, each with a price in currency (the value lines' currency, default USD), and an onSubmit. The rate is from.price / to.price unless you pass rate: a number is the live quote for the current pair (refresh it when the pair changes), a function is asked per pair. convert computes the received amount itself, for fees or slippage.

'use client'

import { CurrencySwap, type CurrencySwapAsset } from '@youj/react'

const assets: CurrencySwapAsset[] = [
  { id: 'btc', symbol: 'BTC', name: 'Bitcoin', price: 64250 },
  { id: 'usdc', symbol: 'USDC', name: 'USD Coin', price: 1, decimals: 2 },
]

export function Swap() {
  return (
    <CurrencySwap
      assets={assets}
      defaultAmount={0.25}
      pendingLabel="Swapping…"
      successLabel="Swapped"
      onSubmit={({ from, to, amount }) => placeOrder(from.id, to.id, amount)}
    />
  )
}

The pair and the amount are each a value trio: from/defaultFrom/onFromChange, to/defaultTo/onToChange and amount/defaultAmount/onAmountChange. onSwap fires on a flip with the new pair. onSubmit is awaited as an Async Button's action: resolve for success, reject or throw for an error. Without onSubmit there is no action button.

Amounts and values format in the provider's locale (or locale). An asset's decimals caps its fraction digits; otherwise amounts show 4, or 4 significant digits below 1.

Reference

Propsassets, from, to, amount, rate, convert, currency, onSwap, onSubmit…

Props

CurrencySwap props
PropType
assetsRequiredCurrencySwapAsset[]

The currencies both pickers offer. At least two.

–
fromstring

The id of the currency paid with (controlled).

–
defaultFromstring

The starting currency paid with. Default: the first asset.

–
onFromChange(id: string) => void–
tostring

The id of the currency received (controlled).

–
defaultTostring

The starting currency received. Default: the second asset.

–
onToChange(id: string) => void–
amountnumber | null

The amount paid, in from (controlled). null is empty.

–
defaultAmountnumber | null

default null

null
onAmountChange(amount: number | null) => void–
ratenumber | ((from: CurrencySwapAsset, to: CurrencySwapAsset) => number | undefined)

How many to one from buys. A number is the quote for the current pair (the app refreshes it when the pair changes); a function is asked per pair. Default: from the assets' prices.

–
convert(amount: number, from: CurrencySwapAsset, to: CurrencySwapAsset) => number | undefined

The received amount, for conversions a rate can't express (fees, slippage). Wins over rate for the amount; the rate line still reads rate.

–
currencystring

The currency the value lines are in (ISO 4217).

default 'USD'

'USD'
onSwap(pair: { from: string; to: string }) => void

Fires on a flip, with the new pair.

–
onSubmit(swap: CurrencySwapSubmit) => unknown

The primary action, awaited as an AsyncButton's onAction: resolve for success, reject or throw for an error. Without it there is no action button.

–
submitLabelReactNode

The action's label.

default "Swap"

"Swap"
pendingLabelstring

Shown while onSubmit runs, and announced.

–
successLabelstring | ((result: unknown) => string)

Shown once onSubmit resolves, and announced.

–
errorLabelstring | ((error: unknown) => string)

Shown when onSubmit rejects, and announced assertively.

–
localestring

Default: the YoujProvider locale.

–
disabledboolean

default false

false
soundboolean

With sound on (YoujProvider’s sound), a flip sounds tap and a new currency select; the action sounds as an AsyncButton does. false keeps it silent.

default true

true
Other props pass through: extends ComponentProps<'div'>.

Data attributes

The root carries data-disabled when disabled. The parts are named in data-slot: currency-swap-pay and currency-swap-receive (the two sides), currency-swap-amount (the field), currency-swap-received (the result), currency-swap-picker (each currency picker), currency-swap-mark (a currency's mark), currency-swap-value (each value line), currency-swap-flip, currency-swap-rate and currency-swap-submit. The action is an Async Button with its own data-state.

MotionRolling digits, the flip · reduced motion

Motion

Motion tokens
MomentTokens
Resultthe received amount rolls digit by digit to its new figure (motion’s Counter)
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Flipeach side’s amount and currency glide into the other’s place, from a touch smaller
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Flip buttonthe arrows turn half a circle per flip
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Actionthe button runs the Async Button’s pending, success and error states
no token

Reduced motion

The figure changes and the sides swap at once; the arrows don't turn.

Reduced motion

Every change lands at once. The card keeps its size throughout: the pickers have a fixed width, and long figures truncate.

AccessibilityLabels, keyboard, announcements

Accessibility

  • The amount is a text field labeled “You pay”; it parses and formats in the locale, and ArrowUp/Down step it.
  • Each currency picker is a select named “Currency to pay with” or “Currency to receive”, with typeahead and the arrow keys.
  • The flip is a button named “Swap currencies”. Focus stays on it after a flip.
  • The result is an output labeled “You receive”. Once it rests it is announced politely (“You receive 2,549.9 HYPE”), not on every keystroke.
  • The action is an Async Button: pending and success are announced politely, an error assertively.
  • With sound on, a flip plays tap and a new currency select; the action sounds as an Async Button does. sound={false} keeps it silent.

Bitcoin, Ethereum, Solana and USDC logos are trademarks of their owners, shown for illustration only.