Currency Input
Ant Compact money field with ISO 4217 selector, blur-time formatting, and currency-format registry dependency.
Controlled money field with Ant Compact layout: searchable ISO selector on the left (form only), locale-aware amount on the right, blur-time formatting. value is number | null; onValueChange fires on blur after parse.
Money helpers live in the separate Currency Format registry item. currency-input declares registryDependencies including input, popover, command, fui-i18n, and currency-format so the CLI installs helpers when you add the input.
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/currency-input.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/currency-input.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/currency-input.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/currency-input.jsonWith a namespace: npx shadcn@latest add @f-ui/currency-input — pulls currency-format via registryDependencies.
Usage
import { CurrencyInput } from '@/components/f-ui/currency-input/currency-input';
<CurrencyInput
value={amount}
onValueChange={setAmount}
currency={currency}
onCurrencyChange={setCurrency}
/>
<CurrencyInput readOnly value={amount} currency="USD" />Omit onCurrencyChange to lock the currency and hide the selector. Use surface="compact" for data-table inline edit (no selector).
Examples
Switchable, Fixed, and Read-Only
Default form surface with blur-time formatting.
Switchable currency
USD · 1234.56
Fixed currency (USD only)
Read-only display
"use client";
import { useState } from "react";
import { CurrencyInput } from "@/components/f-ui/currency-input/currency-input";
export function CurrencyInputDemo() {
const [switchableAmount, setSwitchableAmount] = useState<number | null>(1234.56);
const [switchableCurrency, setSwitchableCurrency] = useState("USD");
const [fixedAmount, setFixedAmount] = useState<number | null>(250);
return (
<div className="max-w-sm space-y-6">
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Switchable currency</p>
<CurrencyInput
value={switchableAmount}
onValueChange={setSwitchableAmount}
currency={switchableCurrency}
onCurrencyChange={setSwitchableCurrency}
aria-label="Amount"
/>
<p className="text-muted-foreground text-xs tabular-nums">
{switchableCurrency} · {switchableAmount ?? "null"}
</p>
</div>
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Fixed currency (USD only)</p>
<CurrencyInput
value={fixedAmount}
onValueChange={setFixedAmount}
currency="USD"
aria-label="Amount in USD"
/>
</div>
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Read-only display</p>
<CurrencyInput readOnly value={switchableAmount} currency={switchableCurrency} />
</div>
</div>
);
}Formily
One FormField with the connected CurrencyInput from Form (Formily) binds the amount; reactions syncs the sibling currency path with the selector (two-path pattern).
"use client";
import { useMemo, useState } from "react";
import { CurrencyInput } from "@/components/f-ui/formily/connects/currency-input";
import { Form } from "@/components/f-ui/formily/form";
import { FormActions } from "@/components/f-ui/formily/form-actions";
import { FormField } from "@/components/f-ui/formily/form-field";
import { createForm } from "@/components/f-ui/formily/internals/create-form";
import { Button } from "@/components/ui/button";
interface PaymentValues {
amount: number | null;
currency: string;
}
export function CurrencyInputFormDemo() {
const form = useMemo(
() =>
createForm<PaymentValues>({
initialValues: { amount: null, currency: "EUR" },
}),
[],
);
const [submitted, setSubmitted] = useState<PaymentValues | null>(null);
return (
<div className="w-full max-w-sm space-y-4">
<Form form={form} onSubmit={(values) => setSubmitted({ ...values })}>
<FormField
name="amount"
label="Amount"
required
description="Currency selector syncs the sibling currency field"
component={[CurrencyInput, { placeholder: "0.00" }]}
reactions={(field) => {
const currency = field.query(".currency").value() as string;
field.setComponentProps({
currency,
onCurrencyChange: (next: string) =>
field.form.setValuesIn("currency", next),
});
}}
/>
<FormActions>
<Button type="submit" size="sm">
Submit payment
</Button>
</FormActions>
</Form>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}Compact Surface
Table cell density; currency comes from column metadata, not the selector.
Table cell density — selector hidden, currency locked from column
Parsed value: 9876.54
"use client";
import { useState } from "react";
import { CurrencyInput } from "@/components/f-ui/currency-input/currency-input";
export function CurrencyInputCompactDemo() {
const [amount, setAmount] = useState<number | null>(9876.54);
return (
<div className="max-w-[12rem] space-y-2">
<p className="text-muted-foreground text-xs">
Table cell density — selector hidden, currency locked from column
</p>
<CurrencyInput
surface="compact"
value={amount}
onValueChange={setAmount}
currency="EUR"
aria-label="Amount"
/>
<p className="text-muted-foreground text-xs tabular-nums">
Parsed value: {amount ?? "null"}
</p>
</div>
);
}ISO 4217 and Symbols
- Codes —
listCurrencies()usesIntl.supportedValuesOf('currency')(ECMA-402 / ISO 4217). A static fallback list is used in tests and on older runtimes. - Symbols — ISO 4217 defines alphabetic codes and minor units only; it does not standardize display glyphs.
getCurrencyMetaresolves symbols from a vendored code → symbol table (sourced from XE currency symbols via currency-symbol-map, plus newer ISO codes such as SLE, XCG, XDR, ZWG).Intlis used only as a fallback when a code is missing from the table. - Caveats — Many currencies share
$; some newer codes have no widely adopted Unicode symbol and may show the ISO code (e.g. ZWG). Selector chrome is indicative UI, not legal tender metadata. - Search (v1) — Filter by code and symbol only; localized currency names are not indexed.
- Option row —
USD · $format in the selector dropdown.
Form and Table Inline Edit
Formily
Use Form (Formily)'s FormField with the connected CurrencyInput — it wires value / onValueChange on the amount path; a reactions callback syncs a sibling currency path through the selector. onValueChange fires on blur, which pairs with validator={{ triggerType: "onBlur", ... }}. Raw Formily Field wiring remains available for fully custom layouts. See the form demo above.
Data Table
Inline edit uses NumberValueEditor with surface="compact" and a locked currency from column presentation. The adapter maps the table's string draft to number | null. Cell display uses formatMoneyForDisplay from Currency Format. See docs/superpowers/specs/2026-06-10-f-ui-field-input-standard-design.md for the cross-input contract.
Composition
CurrencyInput
└── root (div)
└── CurrencyInputControl
├── CurrencyInputSelector (default surface, not currencyLocked)
└── CurrencyInputAmountField (Input)Read-only mode renders CurrencyInputControl with a formatted span instead of the amount field.
API Reference
Props
| Prop | Type | Default |
|---|---|---|
value | number | null | — |
onValueChange | (value: number | null) => void | — |
currency | string | — |
onCurrencyChange | (currency: string) => void | — |
defaultCurrency | string | "USD" when uncontrolled |
surface | "default" | "compact" | "default" |
locale | string | i18n provider |
allowedCurrencies | string[] | all ISO codes |
allowNegative | boolean | false |
readOnly | boolean | false |
disabled | boolean | false |
placeholder | string | locale-aware |
className | string | — |
classNames | Partial<Record<CurrencyInputSlot, string>> | — |
autoFocus | boolean | — |
id / name | string | — |
aria-label / aria-labelledby / aria-describedby | string | — |
aria-invalid | boolean | — |
t | CurrencyInputTranslateFn | built-in bundle |
currencyLocked when currency is set without onCurrencyChange — selector hidden.
Slots
| Slot | Applied to |
|---|---|
root | Outer wrapper |
control | CurrencyInputControl shared-border row |
amount | Amount Input (editable) or read-only span |