f-ui
Components

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.json
npx shadcn@latest add https://ui.isaacfei.com/r/currency-input.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/currency-input.json
bun x shadcn@latest add https://ui.isaacfei.com/r/currency-input.json

With 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

$1,234.56
"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).

Currency selector syncs the sibling currency field

"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

  1. CodeslistCurrencies() uses Intl.supportedValuesOf('currency') (ECMA-402 / ISO 4217). A static fallback list is used in tests and on older runtimes.
  2. Symbols — ISO 4217 defines alphabetic codes and minor units only; it does not standardize display glyphs. getCurrencyMeta resolves 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). Intl is used only as a fallback when a code is missing from the table.
  3. 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.
  4. Search (v1) — Filter by code and symbol only; localized currency names are not indexed.
  5. Option rowUSD · $ 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

PropTypeDefault
valuenumber | null
onValueChange(value: number | null) => void
currencystring
onCurrencyChange(currency: string) => void
defaultCurrencystring"USD" when uncontrolled
surface"default" | "compact""default"
localestringi18n provider
allowedCurrenciesstring[]all ISO codes
allowNegativebooleanfalse
readOnlybooleanfalse
disabledbooleanfalse
placeholderstringlocale-aware
classNamestring
classNamesPartial<Record<CurrencyInputSlot, string>>
autoFocusboolean
id / namestring
aria-label / aria-labelledby / aria-describedbystring
aria-invalidboolean
tCurrencyInputTranslateFnbuilt-in bundle

currencyLocked when currency is set without onCurrencyChange — selector hidden.

Slots

SlotApplied to
rootOuter wrapper
controlCurrencyInputControl shared-border row
amountAmount Input (editable) or read-only span

On this page