Currency Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React currency input: a money field formatted per locale as you type, with an optional currency selector and quick amount presets.
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.
Currency Input is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add currency-input, together with Button, Field, Select, Number format, useLocale and 3 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add currency-inputCurrency Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import CurrencyInput from @/components/ui/currency-input.
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
labeldefault""ReactNodeRenders the field anatomy around the box and links it to the input with htmlFor. Leave it empty for a bare field.
descriptiondefault""ReactNodeHelper 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.
errordefault""ReactNodeReplaces the description with the animated error and marks the field invalid (red ring, aria-invalid).
requireddefaultfalsebooleanThe mark after the label, and required on the input, so native form validation stops an empty submit.
invaliddefaultfalsebooleanThe 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.
disableddefaultfalsebooleanDims the field to 50%, blocks typing, the currency button and the chips, and leaves the hidden inputs out of the submit.
readOnlydefaultfalsebooleanThe amount stays visible and selectable; typing, the arrows, the currency list and the chips stop.
placeholderdefault""stringHint 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).
namedefault""stringRenders 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.
currencyNamedefault""stringWith a currency selector, a second hidden form field with the chosen code ("EUR").
sizedefault"md""sm" | "md" | "lg"32 / 40 / 48px, like a button. md grows to 44px on touch screens and the text is 16px on phones so iOS never zooms.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The box: a soft fill, an outline, a bottom border that grows a bar on focus, or ghost for tables and toolbars.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape, scaled per size like the button (md is 8 / 10 / 12px); the currency button follows it. Ignored by underline.
currencyDisplaydefault"narrowSymbol""narrowSymbol" | "symbol" | "code" | "name"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").
defaultCurrencydefault"USD"stringThe starting ISO 4217 code while uncontrolled. Its decimals (USD 2, JPY 0) and symbol follow.
localedefault""stringFormats 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".
mindefault-90071992547409numberThe lowest amount. A lower typed amount shows invalid and is clamped on blur; the arrows stop here. Without allowNegative nothing goes below 0 anyway.
maxdefault90071992547409numberThe highest amount, the same way. Anything above Number.MAX_SAFE_INTEGER / 100 cannot be typed at all.
decimalsdefault-1numberFraction digits typed and padded to (0 to 6). The default -1 takes the currency's: USD 2, JPY 0, BHD 3.
allowNegativedefaultfalsebooleanLets a minus in (a balance adjustment, a refund). iOS decimal keypads have no minus key: negatives come from ArrowDown or a hardware keyboard.
stepdefault1numberWhat ArrowUp and ArrowDown add or take away; Shift moves ten steps.
clearabledefaultfalsebooleanAn x at the end while there is an amount; Escape clears too. Focus stays in the field.
selectOnFocusdefaulttruebooleanSelects the whole amount when the field takes focus (a click too), so typing replaces it.
responsivedefaulttruebooleanThe currency list becomes a bottom sheet below 768px.
valuenumber | nullThe 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.
defaultValuedefault"null"number | nullThe starting amount while uncontrolled. A form reset returns to it.
onValueChange(value: number | null) => voidEvery 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>) => voidThe native input event, for keystrokes the field accepts.
currencystringThe controlled ISO 4217 code. Switching currency never converts the amount.
onCurrencyChange(currency: string) => voidWhen a currency is picked from the selector.
currenciesstring[]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.
presetsnumber[]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.
labelsPartial<{ 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).
containerHTMLElement | nullPortal target for the currency list. Defaults to document.body.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional and optionalLabel, labelAction, orientation, labelWidth, hideLabel.
idstringThe input's id. Defaults to useId(); inside a <Field> the Field's id wins.
refRefObject<HTMLInputElement | null> or (node) => voidThe native text input. Every other native prop lands there too: inputMode (decimal), enterKeyHint (done), autoComplete (transaction-amount), onBlur, onKeyDown, aria-* and data-*.
classNamesPartial<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.
classNamestringClasses 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.
Dependencies
Files
- components/ui/currency-input.tsx
FAQ
Is the React Currency Input component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Currency Input?
Run npx wingo-ui@latest add currency-input. It also adds the Wingo UI items it builds on (Button, Field, Select, Number format, useLocale and 3 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/currency-input.
How does Currency Input behave on phones?
The currency list becomes a bottom sheet below 768px. The responsive prop controls this and is on by default.
See also
Used in
More React input components
Articles using this component
- Shadcn Number Input: Steppers, Min, Max and Decimals
Component guides8 min read
- React Currency Input: Intl Formatting Without Caret Jumps
Component guides10 min read
- E-Factura VAT Calculation in TypeScript and React
Build it: screens and apps11 min read
