Skip to content

Financial App

Show negative balances as (1,234.56) instead of -$1,234.56 — standard in double-entry bookkeeping:

import { presets } from "raqam";
import { NumberField } from "raqam";
<NumberField.Root
locale="en-US"
formatOptions={presets.accounting("USD")}
defaultValue={-1234.56}
allowNegative
>
<NumberField.Label>Account balance</NumberField.Label>
<NumberField.Input />
</NumberField.Root>
// Displays: (1,234.56) ← negative shown as parentheses
// Displays: 1,234.56 ← positive shown normally

raqam automatically parses (1,234.56) back to -1234.56 when the user pastes accounting-formatted values.

For monetary inputs, always show exactly two decimal places:

<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
fixedDecimalScale
maximumFractionDigits={2}
defaultValue={0}
>
<NumberField.Input />
</NumberField.Root>
// Always shows: $0.00, $1.23, $1,234.56

Use rawValue + onRawChange to capture the exact string the user typed, bypassing JavaScript floating-point limitations:

import { useState } from "react";
import { NumberField } from "raqam";
function CryptoInput() {
const [raw, setRaw] = useState<string | null>(null);
return (
<NumberField.Root
locale="en-US"
defaultValue={0}
formatValue={(v) => v.toFixed(8)}
parseValue={(s) => ({
value: parseFloat(s),
isIntermediate: s.endsWith(".") || /\.\d*0+$/.test(s),
})}
onRawChange={setRaw}
>
<NumberField.Label>BTC amount</NumberField.Label>
<NumberField.Input style={{ fontFamily: "monospace" }} />
{raw && <p>Raw: {raw}</p>}
</NumberField.Root>
);
}
// Shows: 3.14159265 (8 decimal places)
// raw = "3.14159265" (exact string for big-decimal libraries)

Pass raw to a BigDecimal library (decimal.js, big.js) for precise arithmetic.

Show the converted value in real time using NumberField.Formatted + a read-only instance:

import { useState } from "react";
import { NumberField, useNumberFieldFormat } from "raqam";
const USD_TO_EUR = 0.92;
function CurrencyConverter() {
const [usd, setUsd] = useState<number | null>(100);
const eur = usd !== null ? usd * USD_TO_EUR : null;
const eurFormatted = useNumberFieldFormat(eur ?? 0, {
locale: "de-DE",
formatOptions: { style: "currency", currency: "EUR" },
});
return (
<div>
<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
value={usd}
onChange={setUsd}
minValue={0}
>
<NumberField.Label>USD amount</NumberField.Label>
<NumberField.Input />
</NumberField.Root>
<p>{eurFormatted}</p>
</div>
);
}
<NumberField.Root
locale="en-US"
formatOptions={{ style: "currency", currency: "USD" }}
defaultValue={5000}
minValue={1000}
maxValue={100000}
step={100}
largeStep={1000}
validate={(v) => {
if (v === null) return "Budget is required";
if (v < 1000) return "Minimum budget is $1,000";
if (v > 100000) return "Maximum budget is $100,000";
if (v % 100 !== 0) return "Budget must be in $100 increments";
return true;
}}
>
<NumberField.Label>Annual budget</NumberField.Label>
<NumberField.Group>
<NumberField.Decrement>−$100</NumberField.Decrement>
<NumberField.Input />
<NumberField.Increment>+$100</NumberField.Increment>
</NumberField.Group>
<NumberField.ErrorMessage />
</NumberField.Root>

Know exactly how the user changed the value — useful for analytics:

<NumberField.Root
locale="en-US"
defaultValue={0}
onValueChange={(value, { reason, formattedValue }) => {
analytics.track("number_changed", { value, reason, formattedValue });
// reason: "keyboard" | "wheel" | "paste" | "blur" | "increment" | "decrement" | "scrub"
}}
>
<NumberField.Input />
</NumberField.Root>