Skip to main content
The TokenInput component provides a unified interface for selecting tokens and entering amounts. It includes balance display, USD value calculation, and percentage shortcuts.

Import

import { TokenInput } from '@/components/TokenInput';

Overview

TokenInput is a reusable component used throughout the application for token-related inputs. It combines:
  • Asset Selection: Token picker with search and balance display
  • Amount Input: Numeric input with validation
  • Balance Display: Shows available balance for selected token
  • USD Value: Calculates and displays fiat value
  • Percentage Shortcuts: Quick-select buttons (25%, 50%, 75%, MAX)

Props

label
string
required
Display label for the input field (e.g., “Sell”, “Buy”, “You’re sending”)
assets
Asset[]
required
Array of available assets to choose from
interface Asset {
  aggregatedAssetId: string;
  symbol: string;
  name: string;
  decimals: number;
  aggregatedEntities: AggregatedAssetEntity[];
}
selectedAsset
string
required
The aggregated asset ID of the currently selected token (e.g., “ob:usdc”)
onAssetChange
(value: string) => void
required
Callback fired when the selected asset changes
amount
string
required
The current amount value (human-readable format)
onAmountChange
(e: React.ChangeEvent<HTMLInputElement>) => void
required
Callback fired when the amount input changes
balance
TokenBalance | null
default:"null"
Balance information for the selected token
interface TokenBalance {
  aggregatedAssetId: string;
  balance: string;      // Raw balance in token's base unit
  fiatValue: number;    // USD value of the balance
}
showPercentageButtons
boolean
default:"false"
Whether to show percentage shortcut buttons (25%, 50%, 75%, MAX)
onPercentageClick
(percentage: number) => void
Callback fired when a percentage button is clicked. Receives the percentage (25, 50, 75, or 100)
disabled
boolean
default:"false"
Whether the input is disabled
readOnly
boolean
default:"false"
Whether the amount input is read-only (used for output amounts in swaps)
balances
TokenBalance[]
default:"[]"
Array of all token balances (passed to AssetSelect for balance display)
usdValue
string | null
default:"null"
Override USD value to display (if not provided, calculated automatically)

Usage Examples

Basic Input

import { TokenInput } from '@/components/TokenInput';
import { useState } from 'react';

function MyComponent() {
  const [asset, setAsset] = useState('ob:usdc');
  const [amount, setAmount] = useState('');
  
  return (
    <TokenInput
      label="Amount"
      assets={assets}
      selectedAsset={asset}
      onAssetChange={setAsset}
      amount={amount}
      onAmountChange={(e) => setAmount(e.target.value)}
    />
  );
}

With Balance and Percentage Buttons

<TokenInput
  label="Sell"
  assets={assets}
  selectedAsset={sourceAsset}
  onAssetChange={setSourceAsset}
  amount={amount}
  onAmountChange={(e) => setAmount(e.target.value)}
  balance={sourceBalance}
  showPercentageButtons={true}
  onPercentageClick={(percentage) => {
    if (sourceBalance) {
      const maxAmount = formatTokenAmount(
        sourceBalance.balance,
        selectedAsset.decimals
      );
      const targetAmount = (parseFloat(maxAmount) * percentage / 100).toString();
      setAmount(targetAmount);
    }
  }}
  balances={allBalances}
/>

Read-Only (Output Display)

<TokenInput
  label="Buy"
  assets={assets}
  selectedAsset={targetAsset}
  onAssetChange={setTargetAsset}
  amount={calculatedAmount}
  onAmountChange={() => {}} // No-op for read-only
  balance={targetBalance}
  readOnly={true}
  usdValue="1234.56"
  balances={allBalances}
/>

Features

Amount Validation

The component validates that input is a valid number:
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const value = e.target.value;
  
  // Only allow valid number format
  if (!/^(\d*\.?\d*)?$/.test(value)) return;
  
  // Call parent handler
  onAmountChange(e);
};

USD Value Calculation

Automatically calculates USD value based on balance and amount:
const getUSDValue = () => {
  if (!balance || !amount || !selectedAssetData) return null;
  
  const numericAmount = parseFloat(amount);
  if (isNaN(numericAmount) || numericAmount === 0) return null;
  
  const balanceAmount = parseFloat(
    formatTokenAmount(balance.balance, selectedAssetData.decimals || 18)
  );
  if (balanceAmount === 0) return null;
  
  const pricePerToken = balance.fiatValue / balanceAmount;
  const usdValue = numericAmount * pricePerToken;
  return usdValue.toFixed(2);
};

Balance Display

Shows formatted balance with token symbol:
const formatBalance = (balance: TokenBalance, asset: Asset) => {
  const formattedAmount = formatTokenAmount(balance.balance, asset.decimals || 18);
  const numericAmount = Number(formattedAmount);
  return numericAmount.toFixed(numericAmount < 0.01 ? 6 : 2);
};

Percentage Shortcuts

Optional quick-select buttons:
{showPercentageButtons && balance && selectedAssetData && onPercentageClick && (
  <div className="flex gap-2 px-1">
    {[25, 50, 75, 100].map(percentage => (
      <Button
        key={percentage}
        variant="outline"
        size="sm"
        onClick={() => onPercentageClick(percentage)}
        disabled={disabled}
      >
        {percentage === 100 ? 'MAX' : `${percentage}%`}
      </Button>
    ))}
  </div>
)}

Layout Structure

┌─────────────────────────────────────────┐
│ Label                    [25%][50%][75%][MAX] │
├─────────────────────────────────────────┤
│                                         │
│  1234.56                    [USDC ▼]   │
│  $1,234.56                  1000 USDC  │
│                                         │
└─────────────────────────────────────────┘
  Amount    USD Value    Selector  Balance

Styling

The component uses Tailwind CSS classes and adapts to light/dark themes:
  • Container: bg-muted/50 rounded-2xl p-4 border border-border
  • Input: text-2xl font-medium bg-transparent
  • USD Value: text-sm text-muted-foreground
  • Balance: text-xs text-muted-foreground

Onboarding Support

The amount input includes a data attribute for onboarding:
<Input
  data-onboarding="amount-input"
  // ... other props
/>

Asset Symbol Helper

Extracts display symbol from aggregated asset ID:
const getAssetSymbol = (assetId: string) => {
  return assetId.split(':')[1]?.toUpperCase() || assetId;
};

// Example:
// 'ob:usdc' → 'USDC'
// 'ob:eth' → 'ETH'

Dependencies

import { AssetSelect } from '@/components/AssetSelect';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

Best Practices

Always validate the amount on the parent component before using it in API calls. The TokenInput component only validates format, not value constraints.
When using percentage buttons, ensure the onPercentageClick handler accounts for token decimals correctly to avoid precision errors.
The readOnly prop is useful for output fields (like the destination amount in a swap) where users shouldn’t edit the value directly.

Build docs developers (and LLMs) love