<bpdm/ui />
Form

Money Input

Currency + locale-aware money input — grouped per locale, precise via bignumber.js, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Money Input is a currency- and locale-aware amount field. The display is grouped for the locale (e.g. en-IN1,00,000) with the right currency symbol and decimal count, while the stored value stays a precise numeric string (bignumber.js — no float rounding). It's editable as a plain number on focus and formatted on blur. In React it's <MoneyInput>; in Angular it's <bpdm-money-input>.

Usage

Give it a currency, a locale, and a starting amount.

amount.tsx
import { useState } from 'react';
import { MoneyInput } from '@bpdm/ui/money-input';

export function Amount() {
  const [value, setValue] = useState('1234.5');
  return <MoneyInput currency="USD" locale="en-US" value={value} onValueChange={setValue} />;
}
amount.ts
import { Component, signal } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';

@Component({
  selector: 'amount',
  imports: [BpdmMoneyInput],
  template: `<bpdm-money-input currency="USD" locale="en-US" [(value)]="value" />`,
})
export class Amount {
  readonly value = signal('1234.5');
}

Currencies & locales

The same stored amount renders differently per currency + locale — symbol, grouping, and decimal count all follow the locale (note en-IN's 1,00,000 grouping and JPY's zero decimals).

USD
EUR
INR
JPY
currencies.tsx
import { MoneyInput } from '@bpdm/ui/money-input';

export function Currencies() {
  return (
    <div className="flex w-72 flex-col gap-3">
      <MoneyInput currency="USD" locale="en-US" defaultValue="100000" /> {/* $100,000.00 */}
      <MoneyInput currency="EUR" locale="de-DE" defaultValue="100000" /> {/* 100.000,00 € */}
      <MoneyInput currency="INR" locale="en-IN" defaultValue="100000" /> {/* ₹1,00,000.00 */}
      <MoneyInput currency="JPY" locale="ja-JP" defaultValue="100000" /> {/* ¥100,000 */}
    </div>
  );
}
currencies.ts
import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';

@Component({
  selector: 'currencies',
  imports: [BpdmMoneyInput],
  template: `
    <div class="flex w-72 flex-col gap-3">
      <bpdm-money-input currency="USD" locale="en-US" defaultValue="100000" />
      <bpdm-money-input currency="EUR" locale="de-DE" defaultValue="100000" />
      <bpdm-money-input currency="INR" locale="en-IN" defaultValue="100000" />
      <bpdm-money-input currency="JPY" locale="ja-JP" defaultValue="100000" />
    </div>
  `,
})
export class Currencies {}

Precision

The stored value is a string folded through bignumber.js, so large amounts keep every digit — no float rounding, no exponential notation.

precision.tsx
import { MoneyInput } from '@bpdm/ui/money-input';

export function Precision() {
  return <MoneyInput currency="USD" locale="en-US" defaultValue="123456789012.34" />;
}
precision.ts
import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';

@Component({
  selector: 'precision',
  imports: [BpdmMoneyInput],
  template: `<bpdm-money-input currency="USD" locale="en-US" defaultValue="123456789012.34" />`,
})
export class Precision {}

Sizes

size sets the height and text scale — sm, md (default), or lg.

sizes.tsx
import { MoneyInput } from '@bpdm/ui/money-input';

export function Sizes() {
  return (
    <div className="flex w-64 flex-col gap-3">
      <MoneyInput size="sm" currency="USD" defaultValue="2500" />
      <MoneyInput size="md" currency="USD" defaultValue="2500" />
      <MoneyInput size="lg" currency="USD" defaultValue="2500" />
    </div>
  );
}
sizes.ts
import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';

@Component({
  selector: 'sizes',
  imports: [BpdmMoneyInput],
  template: `
    <div class="flex w-64 flex-col gap-3">
      <bpdm-money-input size="sm" currency="USD" defaultValue="2500" />
      <bpdm-money-input size="md" currency="USD" defaultValue="2500" />
      <bpdm-money-input size="lg" currency="USD" defaultValue="2500" />
    </div>
  `,
})
export class Sizes {}

States

aria-invalid turns the border destructive-red; disabled dims the field and blocks input.

states.tsx
import { MoneyInput } from '@bpdm/ui/money-input';

export function States() {
  return (
    <div className="flex flex-col gap-3">
      <MoneyInput aria-invalid currency="USD" defaultValue="0" />
      <MoneyInput disabled currency="USD" defaultValue="2500" />
    </div>
  );
}
states.ts
import { Component } from '@angular/core';
import { BpdmMoneyInput } from '@bpdm/ng';

@Component({
  selector: 'states',
  imports: [BpdmMoneyInput],
  template: `
    <div class="flex flex-col gap-3">
      <bpdm-money-input aria-invalid="true" currency="USD" defaultValue="0" />
      <bpdm-money-input disabled currency="USD" defaultValue="2500" />
    </div>
  `,
})
export class States {}

API

MoneyInput / bpdm-money-input

The stored value is always a raw numeric string (e.g. "1234.50"); only the display is formatted.

PropTypeDefaultDescription
currencystring (ISO 4217)"USD"Currency code — sets the symbol and decimal count.
localestring (BCP 47)"en-US"Locale — sets grouping and symbol placement.
valuestringControlled value. React: pair with onValueChange. Angular: two-way [(value)].
defaultValuestring""Uncontrolled initial value.
onValueChange (React)(value: string) => voidCalled with the raw numeric string on every change.
placeholderstringPlaceholder shown when empty.
allowNegativebooleanfalsePermit negative amounts.
sizesm | md | lgmdHeight + text scale.
disabledbooleanfalseDisable the field.
namestringNative name for form submission (forwarded to the <input>).
aria-invalidbooleanfalseInvalid state — destructive border, announced by assistive tech.
aria-label / aria-describedbystringAccessible name / description, forwarded to the <input>.
idstringField id (for <label> association).
className (React) / class (Angular)stringExtra classes on the wrapper.

In React, MoneyInput forwards its ref to the underlying <input> and spreads any other native <input> prop (required, autoComplete, data-*, onFocus, onBlur, …) onto it, so it drops into forms and test harnesses unchanged. In Angular, required, autoComplete, name, aria-label, and aria-describedby are dedicated inputs on <bpdm-money-input>.

Accessibility

  • The currency symbol is a decorative adornment (aria-hidden, select-none, muted) — it is not announced. So give the field a name that carries the currency (e.g. aria-label="Amount in USD") via a <label> (htmlFor/id) or aria-label; the symbol alone won't tell a screen-reader user which currency they're entering.
  • The field uses inputmode="decimal", so mobile shows a numeric keypad.
  • Invalid state is set with aria-invalid — it colours the border and is announced (never colour-only). Pair it with aria-describedby pointing at your error text.
  • Labelling attributes reach the <input> directly: aria-label, aria-describedby, and name are forwarded in both frameworks (React spreads every native <input> prop; Angular exposes them as inputs).

Internationalization

  • Locale-aware formatting is the core i18n feature: currency + locale drive the grouping, symbol placement, and decimal count via Intl.NumberFormat (en-IN1,00,000, de-DE100.000,00 €, JPY → no decimals). The stored value stays a precise numeric string.
  • The currency symbol is decorative, so give the field a translated accessible name that includes the currency. RTL-safe: alignment uses a logical property (text-end), so it mirrors under dir="rtl". See the Internationalization guide.

On this page