Dots

Amount Field

A large money input with a currency, help text, quick amounts and a two-sided exchange layout.

Balance $1,250.00
"use client"

import * as React from "react"

The AmountField component is the main input on a send, top-up or exchange screen. It is built on React Aria NumberField, so it parses and formats the amount for the user's locale, opens a decimal keyboard on phones and validates against minValue and maxValue. Use it for any amount of money the user types freely. For counts and small stepped limits, such as a per-payment limit in steps of $50, use NumberField instead.

Installation

pnpm dlx shadcn@latest add @dots/amount-field

Usage

import {
  AmountField,
  AmountFieldCurrency,
  AmountFieldDescription,
  AmountFieldGroup,
  AmountFieldInput,
  AmountFieldLabel,
} from "@/components/ui/amount-field"
<AmountField defaultValue={25}>
  <AmountFieldLabel>Amount</AmountFieldLabel>
  <AmountFieldGroup>
    <AmountFieldInput />
    <AmountFieldCurrency>USD</AmountFieldCurrency>
  </AmountFieldGroup>
  <AmountFieldDescription>Balance $1,250.00</AmountFieldDescription>
</AmountField>

Composition

Use the following composition to build an amount field:

AmountField
├── AmountFieldLabel
├── AmountFieldGroup
│   ├── AmountFieldInput
│   └── AmountFieldCurrency or AmountFieldCurrencySelect
├── AmountFieldDescription
├── AmountFieldError
└── AmountFieldPresets
    └── AmountFieldPreset

Use AmountFieldPair to link two fields by a rate, with a swap button between them:

AmountFieldPair
├── AmountField (variant="inset")
├── AmountFieldSwap
└── AmountField (variant="inset")

Features

  • Locale-aware parsing and formatting through React Aria NumberField, with a decimal keyboard on phones
  • minValue defaults to 0 and scroll-wheel changes are off, so the amount never changes by accident
  • A fixed currency, or a currency pill that opens a list of currencies with their balances
  • Quick amount chips that set the amount and show as pressed while it matches
  • onInputValueChange reports every keystroke, for live results such as the other side of an exchange
  • An inset variant and AmountFieldPair for two-sided exchanges with a swap button
  • Validation and an error message read with the field

Currency

Use AmountFieldCurrency when the currency is fixed. Use AmountFieldCurrencySelect when the user picks the balance the money comes from. Each option can show a balance in description and can be turned off with isDisabled.

USD
Paid from your USD balance
Pick the balance the money comes from
"use client"

import * as React from "react"

Quick amounts

Put AmountFieldPresets inside the field and add one AmountFieldPreset per amount. A preset sets the amount when pressed and shows as pressed while the amount equals its value. When the label is a share such as "25%", pass an aria-label that says the amount too.

USD
Arrives in a few minutes
"use client"

import {
<AmountFieldPresets>
  <AmountFieldPreset value={312.5} aria-label="25%, $312.50">
    25%
  </AmountFieldPreset>
  <AmountFieldPreset value={1250} aria-label="Max, $1,250.00">
    Max
  </AmountFieldPreset>
</AmountFieldPresets>

Exchange

Wrap two inset fields in AmountFieldPair and place AmountFieldSwap between them. onChange fires only when the amount is committed, on blur or Enter. Use onInputValueChange to update the other side while the user types. It reports NaN while the field is empty.

Balance $1,250.00
1 USD = 0.9210 EUR
"use client"

import * as React from "react"

Invalid

Set isInvalid and render AmountFieldError in place of the description to say what to fix. Offer a way out, such as sending the whole balance.

USD
$150.00 more than you have.
"use client"

import * as React from "react"

Disabled and read-only

Set isDisabled when the amount cannot be used right now, or isReadOnly when it is set elsewhere. Quick amounts follow the field and turn off in both cases.

USD
Transfers are paused
USD
Set by your savings plan
"use client"

import {

Accessibility

AmountField is a React Aria NumberField. The input is a text field with numeric input mode. The label, description and error are linked to the input, and the input is marked invalid while isInvalid is set. The error is read when the input gets focus, so move focus to it after a failed check (see below).

Label the field

Always render AmountFieldLabel, or pass aria-label to AmountField. AmountFieldCurrencySelect adds the field label to its own name, so it is read as "You send, Currency, USD".

Say the amount on share presets

A preset labeled "25%" does not say how much money it sets. Pass an aria-label with the amount, such as "25%, $312.50".

Focus after a failed check

AmountFieldInput accepts a ref. When a check on submit fails, move focus back to the input so the user lands on the error.

const inputRef = React.useRef<HTMLInputElement>(null)
 
<AmountFieldInput ref={inputRef} />
 
// After a failed check
inputRef.current?.focus()

Label the swap button

AmountFieldSwap is icon-only and is named "Swap currencies" by default. Pass aria-label if it swaps something else.

API Reference

AmountField

The root. Accepts all React Aria NumberField props. className takes a string and children takes the parts below.

PropTypeDefaultDescription
variant"default" | "inset""default"inset puts the label, amount and help text on the surface.
valuenumber-The amount (controlled). NaN means empty.
defaultValuenumber-The amount (uncontrolled).
onChange(value: number) => void-Called when the amount is committed, on blur or Enter.
onInputValueChange(value: number) => void-Called on every accepted keystroke with the amount so far. NaN while empty.
formatOptionsIntl.NumberFormatOptions{ maximumFractionDigits: 2 }How the amount is formatted and parsed.
minValuenumber0The smallest amount allowed.
maxValuenumber-The largest amount allowed.
isWheelDisabledbooleantrueTurns off changing the amount with the scroll wheel.
isInvalidboolean-Marks the field invalid and shows AmountFieldError.
isDisabledboolean-Disables the field and its quick amounts.
isReadOnlyboolean-Makes the field read-only and disables its quick amounts.
classNamestring-Additional classes to apply to the root.

AmountFieldLabel

The field label. Renders a Label and accepts all of its props.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the label.

AmountFieldGroup

The surface that holds the amount and the currency. Accepts React Aria Group props.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the surface.

AmountFieldInput

The amount input. Accepts React Aria Input props.

PropTypeDefaultDescription
placeholderstring"0"Shown while the field is empty.
refReact.Ref<HTMLInputElement>-Use it to focus the input, e.g. after an error.
classNamestring-Additional classes to apply to the input.

AmountFieldCurrency

A fixed currency on the surface, such as a code or a symbol. Accepts all <span> props.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the currency.

AmountFieldCurrencySelect

A currency pill that opens a list of currencies. Put it inside AmountFieldGroup. Accepts React Aria Select props except the collection and selection props listed here.

PropTypeDefaultDescription
currenciesAmountFieldCurrencyOption[]-The currencies to choose from. Required.
valuestring | null-The selected currency code (controlled).
defaultValuestring | null-The selected currency code (uncontrolled).
onChange(code: string) => void-Called with the code of the picked currency.
placeholderstring"Currency"Shown before a currency is picked.
aria-labelstring"Currency"Name of the pill. The field label is added to it.
disabledKeysIterable<Key>-Codes to disable, in addition to isDisabled ones.
classNamestring-Additional classes to apply to the select.

AmountFieldCurrencyOption:

FieldTypeDescription
codestringISO 4217 or token code, e.g. "EUR". Used as the key.
namestringe.g. "Euro".
iconReact.ReactNodeA monochrome flag or coin. Without it the code is shown in a round chip.
descriptionReact.ReactNodeQuiet text at the end of the row, e.g. the balance.
isDisabledbooleanTurns the option off.

AmountFieldDescription

Help under the amount, such as the balance or the exchange rate. Read with the field. Accepts React Aria Text props except slot.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the description.

AmountFieldError

Says what to fix. Shown while the field is invalid. Accepts React Aria FieldError props.

PropTypeDefaultDescription
childrenReact.ReactNode-The error message.
classNamestring-Additional classes to apply to the error.

AmountFieldPresets

A row of quick amounts. Put it inside AmountField. Accepts all <div> props.

PropTypeDefaultDescription
aria-labelstring"Quick amounts"Name of the group.
classNamestring-Additional classes.

AmountFieldPreset

A quick amount. Renders a Toggle and accepts its props except isSelected, defaultSelected and onChange.

PropTypeDefaultDescription
valuenumber-The amount to set. Rounded to the fraction digits the field keeps. Required.
isDisabledboolean-Turns the preset off. It is also off while the field is disabled or read-only.
classNamestring-Additional classes to apply to the preset.

AmountFieldPair

Stacks two inset amount fields linked by a rate. Accepts all <div> props.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the pair.

AmountFieldSwap

Swaps the two sides of an AmountFieldPair. Renders a Button and accepts its props except children, variant and size.

PropTypeDefaultDescription
onPress(e: PressEvent) => void-Called when the swap is pressed.
aria-labelstring"Swap currencies"Name of the button.
classNamestring-Additional classes.