Skip to content

CurrencyInput

MD · 1950B gzip budget

Numeric input with currency symbol and locale formatting.

Category
Forms & input
Budget
md, several states or a live subscription
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Currency: USD

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/currency-input.json

Copies the source into your project. Pulls in 3 shared registry items: cn, use-locale, locale.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useId, useMemo, useState } from "react";
import { cn } from "../lib/cn";
import { useLocale } from "../lib/use-locale";

export interface CurrencyInputProps {
  value: number | null;
  onChange: (value: number | null) => void;
  label: string;
  currency?: string;
  locale?: string;
  className?: string;
}

function parseLocaleNumber(input: string, locale: string): number | null {
  const trimmed = input.trim();
  if (!trimmed) return null;

  const parts = new Intl.NumberFormat(locale, { style: "currency", currency: "USD" }).formatToParts(12345.6);
  const group = parts.find((p) => p.type === "group")?.value ?? ",";
  const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";

  const normalised = trimmed
    .split(group).join("")
    .replace(/\s/g, "")
    .replace(decimal, ".")
    .replace(/[^0-9.\-]/g, "");

  const parsed = Number.parseFloat(normalised);
  return Number.isFinite(parsed) ? parsed : null;
}

export function CurrencyInput({ value, onChange, label, currency = "USD", locale: localeProp, className }: CurrencyInputProps) {
  const id = useId();
  const { locale: ctxLocale } = useLocale();
  const locale = localeProp || ctxLocale;
  const [draft, setDraft] = useState<string | null>(null);

  const formatted = useMemo(() => {
    if (value === null) return "";
    return new Intl.NumberFormat(locale, { style: "currency", currency }).format(value);
  }, [value, locale, currency]);

  const symbol = useMemo(() => {
    const parts = new Intl.NumberFormat(locale, { style: "currency", currency }).formatToParts(0);
    return parts.find((p) => p.type === "currency")?.value ?? currency;
  }, [locale, currency]);

  return (
    <div className={cn("flex flex-col gap-1.5 text-start", className)}>
      <label htmlFor={id} className="text-sm font-medium">
        {label}
      </label>
      <div className="relative">
        <span className="absolute start-3 top-1/2 -translate-y-1/2 text-sm text-neutral-500 dark:text-neutral-400" aria-hidden="true">
          {symbol}
        </span>
        <input
          id={id}
          type="text"
          inputMode="decimal"
          value={draft ?? formatted}
          onFocus={() => setDraft(value === null ? "" : String(value))}
          onChange={(e) => setDraft(e.target.value)}
          onBlur={() => {
            onChange(draft === null ? value : parseLocaleNumber(draft, locale));
            setDraft(null);
          }}
          aria-label={`${label} in ${currency}`}
          className="w-full rounded-md border border-neutral-300 bg-white ps-8 pe-3 py-2 text-base tabular-nums focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-neutral-700 dark:bg-neutral-950"
        />
      </div>
      <p className="text-xs text-neutral-500 dark:text-neutral-400">
        Currency: {currency}
      </p>
    </div>
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation