Money input
An amount of money with its currency, typed with a comma or a dot, and shown in the crew's own currency.
On this page
Code
import { MoneyInput, type Money } from "@bosunkit/components/money-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A diesel receipt in Turkish lira on a vessel that keeps its accounts in euros: the currency is chosen at the start " +
"of the field, the amount takes a comma or a dot before the kuruş, and the amount in euros at the ECB rate shows " +
"under it.";
// The European Central Bank's reference rates bosund holds for the day: how many units one euro buys.
const RATES: Readonly<Record<string, number>> = { GBP: 0.8571, TRY: 35.62, USD: 1.0843, THB: 38.9, CHF: 0.9402 };
export default function Showcase(): ReactElement {
const [value, setValue]: [Money, Dispatch<SetStateAction<Money>>] = useState<Money>({
amount: 5385.5,
currency: "TRY",
});
return (
<div style={{ width: "100%", maxWidth: 400 }}>
<MoneyInput
label="Amount"
required
value={value}
onValueChange={setValue}
localCurrency="EUR"
rates={RATES}
ratesDate="2026-10-16"
/>
</div>
);
}
A money input is an amount with its currency, as an expense, a fuel fill or a mooring fee is entered. The currency
sits at the start of the outlined box, as a phone number's country does: its code, which opens a list of the
currencies with each one's name in the crew's language. The euro and the currencies the European Central Bank
quotes are offered when the page names none, the set bosund's exchange rates cover. A list of more than ten opens with
the common currencies as a shortcut: the crew's own (localCurrency) first, then the eight traded most in the Bank for
International Settlements' 2022 survey (US dollar, euro, yen, pound, yuan, Australian and Canadian dollars, Swiss
franc), and after them the others, each currency once.
The amount takes digits with a comma or a dot before the cents, whichever the crew's keyboard has. The last comma or
dot is the decimal separator when the other one comes before it (1.234,50 or 1,234.50), or when no more digits
follow it than the currency has decimal places (151,2 or 151.20); any other comma or dot groups thousands, as do
spaces and apostrophes. Yen and the other currencies without decimals take a separator only between thousands. The
field's own currency may be written before or after the amount, by its code or its symbol ("€ 1.250,00", "EUR
12.50"); another currency's is refused, never read as this one. Once the crew leave the field, the amount is written
back the way their language writes it, and an amount it cannot read stays as typed with the problem under it, the
field reporting no amount meanwhile, so a form never saves the one before. An amount is positive unless
allowNegative says it may be a refund or a correction; a minus before it then makes it negative, and the amount takes
the text keyboard on a phone, whose decimal keypad has no minus. A schema form sets it for a field whose min is below
zero.
With localCurrency and rates, an amount in another currency is shown under the field in the crew's own, with the
day of the rates: "≈ €151.20 at the ECB rate of 16 October 2026". rates are the European Central Bank's euro
reference rates, how many units one euro buys, which the page reads from bosund; the kit makes no request. A
currency the rates do not cover is named, and the amount stays in it. The value is the amount in the currency's major
unit (151.2 for €151.20) and the ISO 4217 code.
Examples
Typed with a comma or a dot
The page receives: no amount yet
Code
import { MoneyInput, type Money } from "@bosunkit/components/money-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"An amount typed either way: 151,20 and 151.20 are the same euros, 1.234,50 and 1,234.50 the same thousand, and " +
"the field writes it back the crew's way once they leave it. The page shows what it receives.";
export default function CommaOrDot(): ReactElement {
const [value, setValue]: [Money, Dispatch<SetStateAction<Money>>] = useState<Money>({ amount: null, currency: "EUR" });
return (
<div style={{ width: "100%", maxWidth: 400, display: "grid", gap: "var(--space-sm)" }}>
<MoneyInput label="Mooring fee" value={value} onValueChange={setValue} placeholder="151,20" />
<p style={{ margin: 0, color: "var(--text-dim)" }}>
The page receives: {value.amount === null ? "no amount yet" : `${String(value.amount)} ${value.currency}`}
</p>
</div>
);
}
No rate for the currency
Code
import { MoneyInput } from "@bosunkit/components/money-input";
import type { ReactElement } from "react";
export const description: string =
"A receipt in a currency the day's rates do not cover: the field says so, and keeps the amount in that currency.";
// The European Central Bank's reference rates bosund holds for the day: how many units one euro buys.
const RATES: Readonly<Record<string, number>> = { GBP: 0.8571, TRY: 35.62, USD: 1.0843, CHF: 0.9402 };
export default function NoRate(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 400 }}>
<MoneyInput
label="Amount"
defaultValue={{ amount: 1200, currency: "XCD" }}
currencies={["EUR", "GBP", "USD", "XCD"]}
localCurrency="EUR"
rates={RATES}
ratesDate="2026-10-16"
/>
</div>
);
}
Invalid
Code
import { MoneyInput } from "@bosunkit/components/money-input";
import type { ReactElement } from "react";
export const description: string =
"An amount with three decimals in euros, as the field shows it once the crew leave it: the error says how many " +
"decimal places the currency has.";
export default function Invalid(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 400 }}>
<MoneyInput
label="Amount"
defaultValue={{ amount: null, currency: "EUR" }}
error="Euro amounts have 2 decimal places. Remove the extra digits."
/>
</div>
);
}
Read-only
Code
import { MoneyInput } from "@bosunkit/components/money-input";
import type { ReactElement } from "react";
export const description: string = "A reconciled expense: the amount and its currency shown, neither changeable.";
export default function ReadOnly(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 400 }}>
<MoneyInput label="Amount" readOnly defaultValue={{ amount: 182.4, currency: "EUR" }} />
</div>
);
}
Disabled
Code
import { MoneyInput } from "@bosunkit/components/money-input";
import type { ReactElement } from "react";
export const description: string = "A field that cannot be used while the form saves.";
export default function Disabled(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 400 }}>
<MoneyInput label="Amount" disabled defaultValue={{ amount: 64, currency: "GBP" }} />
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | – | The field's name, drawn on the outline's top edge. |
| value | Money | undefined | – | The amount and currency, when the caller holds them; pair it with onValueChange. |
| defaultValue | Money | undefined | – | The amount and currency the field starts with when it holds them itself. |
| onValueChange | ((value: Money) => void) | undefined | – | Called with the amount and currency as the crew type an amount or choose a currency. |
| currencies | readonly string[] | undefined | ECB_CURRENCIES | The currencies offered, as ISO 4217 codes; the euro and those the European Central Bank quotes when unset. |
| localCurrency | string | undefined | – | The crew's own currency: an amount in another one is shown in it under the field, at rates, so a receipt in lira
reads in euros too. |
| rates | Readonly<Record<string, number>> | undefined | – | How many units of each currency one euro buys, the European Central Bank's reference rates (bosund's). |
| ratesDate | string | undefined | – | The day the rates are for, an ISO date, named under the field with the converted amount. |
| hint | ReactNode | – | A hint under the field, above the converted amount. |
| error | string | undefined | – | The caller's own problem with the value, shown in place of the hint. |
| required | boolean | undefined | – | |
| allowNegative | boolean | undefined | false | Whether the amount may be negative, a refund or a correction, typed with a minus before it; off by default. The amount then takes the text keyboard on a phone, since the decimal keypad has no minus. |
| disabled | boolean | undefined | – | |
| readOnly | boolean | undefined | – | |
| placeholder | string | undefined | – | Text shown in the empty amount once the label has moved out of its way. |
| id | string | undefined | – | The amount input's id; one is made when unset. |
| name | string | undefined | – | Submits the amount and the currency with a form, as ${name} and ${name}_currency. |
| ref | Ref<HTMLInputElement> | undefined | – | The amount input. |
| className | string | undefined | – | |
| testId | string | undefined | – | Becomes data-testid on the root; the parts are ${testId}--currency and ${testId}--amount. |
| motion | MotionProp | undefined | – | How the field's message moves in and out, as OutlinedField takes it. |
Related
- Outlined field: the box every maritime input draws in.
- Phone input: the field whose country sits at its start, as the currency does here.
- Number input: a plain number with no currency.