# Currency Input

> A React currency input: a money field formatted per locale as you type, with an optional currency selector and quick amount presets.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add currency-input`
- Docs: https://wingo-ui.com/components/currency-input

## Interaction

Type an amount the way you write it: in English 1234.5 becomes 1,234.5 as you type, in Romanian 1.234,5, and either a comma or a dot typed at the end becomes the decimal. Letters never get in, extra decimals are cut, and a currency without cents (JPY) ignores the decimal key. The symbol sits where the locale puts it ($1,234.56, 1.234,56 lei) and is not part of the text. When the field loses focus, or on Enter, the amount is padded to the currency's decimals (1,234.5 becomes 1,234.50) and clamped to min and max; out of range while typing, the field shows the red ring. Focusing the field selects the amount so the next keys replace it. ArrowUp and ArrowDown add or take away one step, Shift ten steps. With clearable, the x or Escape empties the field. With two or more currencies a compact code button (USD) sits at the end of the box: click it or press Enter, Space or the arrows on it to open the list (a bottom sheet on phones), type a letter to jump to a currency, and pick one; the symbol crossfades and the amount stays the same (it is never converted). Quick amounts sit as chips under the field; tapping one fills it in and the matching chip turns solid. Pasting 1 234,56 € or $1,234.56 over the field reads the whole amount. On a phone the decimal keypad opens (either separator is accepted), the text is 16px so iOS never zooms, the md box grows to 44px and the chips and the currency button have 44px hit areas.

## Installation

```bash
npx wingo-ui@latest add currency-input
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/currency-input` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/currency-input.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { CurrencyInput } from "@/components/ui/currency-input"

export function ListingPrice() {
  const [price, setPrice] = useState<number | null>(1250)
  const [currency, setCurrency] = useState("USD")

  return (
    <form className="flex flex-col gap-4">
      <CurrencyInput
        name="price"
        currencyName="currency"
        label="Price"
        description="The price includes tax"
        locale="en-US"
        currencies={["USD", "EUR", "GBP"]}
        currency={currency}
        onCurrencyChange={setCurrency}
        value={price}
        onValueChange={setPrice}
        min={1}
        error={price !== null && price < 1 ? "The minimum price is $1" : undefined}
        required
      />
      <CurrencyInput label="Donation" locale="en-US" presets={[50, 100, 200, 500]} defaultValue={100} clearable />
    </form>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | Renders the field anatomy around the box and links it to the input with htmlFor. Leave it empty for a bare field. |
| `description?` | `ReactNode` | `""` | Helper text under the box, linked with aria-describedby (followed by a hidden "Amount in …" naming the currency). The error takes its place while it shows. |
| `error?` | `ReactNode` | `""` | Replaces the description with the animated error and marks the field invalid (red ring, aria-invalid). |
| `required?` | `boolean` | `false` | The mark after the label, and required on the input, so native form validation stops an empty submit. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. An amount outside min and max sets it on its own until it is back in range. |
| `disabled?` | `boolean` | `false` | Dims the field to 50%, blocks typing, the currency button and the chips, and leaves the hidden inputs out of the submit. |
| `readOnly?` | `boolean` | `false` | The amount stays visible and selectable; typing, the arrows, the currency list and the chips stop. |
| `placeholder?` | `string` | `""` | Hint text while the field is empty. Empty shows zero in the locale and the currency's decimals ("0.00" in en-US, "0" for JPY). |
| `name?` | `string` | `""` | Renders a hidden input with this name and the amount as "1234.56" (dot decimal, empty when null), so FormData gets a number, not the formatted text. The visible input has no name. |
| `currencyName?` | `string` | `""` | With a currency selector, a second hidden form field with the chosen code ("EUR"). |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | 32 / 40 / 48px, like a button. md grows to 44px on touch screens and the text is 16px on phones so iOS never zooms. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The box: a soft fill, an outline, a bottom border that grows a bar on focus, or ghost for tables and toolbars. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape, scaled per size like the button (md is 8 / 10 / 12px); the currency button follows it. Ignored by underline. |
| `currencyDisplay?` | `"narrowSymbol" \| "symbol" \| "code" \| "name"` | `"narrowSymbol"` | How the currency reads next to the amount and on the chips: narrowSymbol is what people write ("$", "€", "lei"), symbol and code print "RON" for lei, name spells it out ("US dollars"). |
| `defaultCurrency?` | `string` | `"USD"` | The starting ISO 4217 code while uncontrolled. Its decimals (USD 2, JPY 0) and symbol follow. |
| `locale?` | `string` | `""` | Formats and parses with this locale: separators, and which side the symbol goes on. Empty uses the nearest LocaleProvider, then the page language. The demo passes "en-US". |
| `min?` | `number` | `-90071992547409` | The lowest amount. A lower typed amount shows invalid and is clamped on blur; the arrows stop here. Without allowNegative nothing goes below 0 anyway. |
| `max?` | `number` | `90071992547409` | The highest amount, the same way. Anything above Number.MAX_SAFE_INTEGER / 100 cannot be typed at all. |
| `decimals?` | `number` | `-1` | Fraction digits typed and padded to (0 to 6). The default -1 takes the currency's: USD 2, JPY 0, BHD 3. |
| `allowNegative?` | `boolean` | `false` | Lets a minus in (a balance adjustment, a refund). iOS decimal keypads have no minus key: negatives come from ArrowDown or a hardware keyboard. |
| `step?` | `number` | `1` | What ArrowUp and ArrowDown add or take away; Shift moves ten steps. |
| `clearable?` | `boolean` | `false` | An x at the end while there is an amount; Escape clears too. Focus stays in the field. |
| `selectOnFocus?` | `boolean` | `true` | Selects the whole amount when the field takes focus (a click too), so typing replaces it. |
| `responsive?` | `boolean` | `true` | The currency list becomes a bottom sheet below 768px. |
| `value?` | `number \| null` |  | The controlled amount in major units (1234.56); null is an empty field. An amount outside min and max shows as given and is flagged invalid. |
| `defaultValue?` | `number \| null` | `"null"` | The starting amount while uncontrolled. A form reset returns to it. |
| `onValueChange?` | `(value: number \| null) => void` |  | Every change: live while typing (unclamped until blur), arrows, chips, clearing, and the rounding when a currency with fewer decimals is picked. |
| `onChange?` | `(event: ChangeEvent<HTMLInputElement>) => void` |  | The native input event, for keystrokes the field accepts. |
| `currency?` | `string` |  | The controlled ISO 4217 code. Switching currency never converts the amount. |
| `onCurrencyChange?` | `(currency: string) => void` |  | When a currency is picked from the selector. |
| `currencies?` | `string[]` |  | Two or more codes show the currency selector: a compact code button at the end of the box, opening a list with the localized names ("US dollar", "Euro") and symbols. With the selector, a suffix symbol is left out (the button names the currency); a prefix one ($) stays. |
| `presets?` | `number[]` |  | Quick amounts as chips under the field ("$50", "$100"), 44px hit areas on touch. The chip equal to the amount is solid and aria-pressed. |
| `labels?` | `Partial<{ currency: string; clear: string; presets: string; amountIn: (currencyName: string) => string }>` |  | Accessible names, merged over English defaults: currency ("Currency", the button reads "Currency: USD" and titles the phone sheet), clear ("Clear"), presets ("Quick amounts", the chips group) and amountIn (n => `Amount in ${n}`, read with the field). |
| `container?` | `HTMLElement \| null` |  | Portal target for the currency list. Defaults to document.body. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional and optionalLabel, labelAction, orientation, labelWidth, hideLabel. |
| `id?` | `string` |  | The input's id. Defaults to useId(); inside a <Field> the Field's id wins. |
| `ref?` | `RefObject<HTMLInputElement \| null> or (node) => void` |  | The native text input. Every other native prop lands there too: inputMode (decimal), enterKeyHint (done), autoComplete (transaction-amount), onBlur, onKeyDown, aria-* and data-*. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "input" \| "symbol" \| "currency" \| "clear" \| "presets" \| "preset", string>>` |  | Extra classes for the inner parts. box targets the field box, input the native element, currency the code button, preset every chip. |
| `className?` | `string` |  | Classes for the outermost element: the Field root when there is a label, description or error, else the box (or the column holding box and chips). |

## Examples

- **Selectable currency**: USD, EUR and GBP in a selector; switching keeps the amount, a hidden field submits the code.
- **Donation presets**: Quick amounts as chips under the field; the matching chip turns solid, and a custom amount validates.
- **Locales**: The same value as 1.234,56 lei in ro-RO and $1,234.56 in en-US; type in either one.
- **Negative amounts**: allowNegative for a balance adjustment, with ArrowUp / ArrowDown in steps of $10.
- **Min and max**: Out of range shows red while typing and is clamped on blur; the error explains the limit.
- **Sizes and variants**: sm, md and lg (lg with a currency selector) and the four box variants.
- **Currency display**: narrowSymbol, symbol, code and name, yen without decimals and a German read-only amount.

## See also

- [Number Input](https://wingo-ui.com/components/number-input) ([Markdown](https://wingo-ui.com/components/number-input.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))

---

Canonical: https://wingo-ui.com/components/currency-input
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
