Token Picker
Responsive modal for picking a crypto asset. Renders as a Dialog on desktop and a bottom Drawer on mobile, with three modes — Pay, Receive, and Pay-With — that swap which sections appear (recent, owned, trending, cash, payment methods).
Install
import { TokenPicker } from '$lib/components/ui/token-picker'; Pay mode
The default. Shows Recent, Your tokens, and Trending. The owned section ranks by USD value (balance × price) descending and only renders rows where the balance is non-zero — perfect for the "spend from" leg of a swap.
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { TokenPicker } from '$lib/components/ui/token-picker';
import type { TokenInfo } from '$lib/components/ui/token-picker';
import { TOKENS } from '$lib/components/trade/trade-data';
let open = $state(false);
let selected = $state<TokenInfo | undefined>(TOKENS[0]);
</script>
<Button variant="outline" onclick={() => (open = true)}>
{selected?.symbol ?? 'Select token'}
</Button>
<TokenPicker
bind:open
tokens={TOKENS}
mode="pay"
title="Pay with"
selectedTokenId={selected?.id}
onSelect={(token) => (selected = token)}
/>Receive mode
Hides the owned section, hides per-token balances, and turns rows that ship multiple chains
into an expandable chain list — so the user can pick USDC on Base rather than just
"USDC". The onSelect callback receives both the token and
the chosen chain id.
<script lang="ts">
let open = $state(false);
let token = $state<TokenInfo | undefined>();
let chain = $state<string | undefined>();
</script>
<TokenPicker
bind:open
tokens={TOKENS}
mode="receive"
title="Receive"
selectedTokenId={token?.id}
selectedChainId={chain}
onSelect={(t, c) => { token = t; chain = c; }}
/>Pay-with mode (mixed surface)
Prepends a Pay with cash section listing the supplied paymentMethods alongside the crypto rows — used inside the Buy flow where the funding source can be a card, a wire,
or one of the user's owned tokens. The onMethodSelect callback fires when the user picks a fiat
method instead of a token.
<script lang="ts">
import { TokenPicker } from '$lib/components/ui/token-picker';
import type { TokenInfo, PaymentMethod } from '$lib/components/ui/token-picker';
import { TOKENS, FIAT_PAY_METHODS } from '$lib/components/trade/trade-data';
let open = $state(false);
let token = $state<TokenInfo | undefined>();
let method = $state<PaymentMethod | undefined>();
</script>
<TokenPicker
bind:open
tokens={TOKENS}
mode="pay-with"
title="Pay with"
paymentMethods={FIAT_PAY_METHODS}
onSelect={(t) => (token = t)}
onMethodSelect={(m) => (method = m)}
/>Inline body
When the modal frame is wrong — e.g. the picker lives inside a multi-step flow that already
owns its surface — import TokenPickerBody directly and
skip the Dialog/Drawer wrapper. Pass hideHeader to drop the
centered title bar, and the body becomes a plain column that fits whatever container holds it.
<script lang="ts">
import { TokenPickerBody } from '$lib/components/ui/token-picker';
import { TOKENS } from '$lib/components/trade/trade-data';
</script>
<div class="rounded-2xl border border-border bg-card">
<TokenPickerBody
tokens={TOKENS}
mode="pay"
title="Pay with"
hideHeader
onSelect={(token) => console.log(token)}
/>
</div>API reference
The default TokenPicker switches between Dialog (desktop) and Drawer (mobile) via the IsMobile rune. For ad-hoc surfaces, compose TokenPickerBody yourself and supply onClose.
| Prop | Type | Default | Description |
|---|---|---|---|
TokenInfo[] | — | Source token catalog. Each token may declare a chainList + per-chain balances; the picker reads balance × price to rank owned tokens. | |
boolean (bindable) | false | Two-way bindable visibility. Selecting a token sets open back to false. | |
'pay' | 'receive' | 'pay-with' | 'pay' | Controls which sections render. Pay shows owned + recent + trending; Receive expands chain lists and hides balances; Pay-With prepends a payment-methods section. | |
string | 'Select token' | Header text inside the modal and the screen-reader-only Drawer.Title / Dialog.Title. | |
string | undefined | — | Pre-highlights the matching row with the selected state. | |
string | undefined | — | When receive mode is active and the selected token has a chain list, marks the active chain row. | |
boolean | false | Renders a disabled "Cash" preview group keyed on FIAT_CURRENCIES — a placeholder for the upcoming on-ramp. | |
PaymentMethod[] | undefined | — | Required when mode="pay-with". Each method is rendered as a row above the token sections. | |
(token: TokenInfo, chainId?: string) => void | — | Fires on token selection. The chainId is populated when the user expands a multi-chain row in receive mode. | |
(method: PaymentMethod) => void | — | Fires when the user picks a row from the Pay-with-cash section in pay-with mode. |