FormatInput
freeFormatInput formats numeric entry as the user types, with numeric, pattern-mask, and custom-format variants over the shared Input.
react-number-format
Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
export default function UsageDemo() {
return (
<div className="mx-auto w-full max-w-md">
<FormatInput.Numeric
placeholder="0.00"
prefix="$"
thousandSeparator
decimalScale={2}
fixedDecimalScale
/>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add FormatInputExamples
Numeric Formats
Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
import Form from '@/components/ui/Form';
export default function NumericFormatsDemo() {
return (
<div className="mx-auto w-full max-w-md">
<Form.Field label="Discount" htmlFor="discount">
<FormatInput.Numeric
id="discount"
placeholder="0.0"
suffix="%"
decimalScale={1}
allowNegative={false}
/>
</Form.Field>
<Form.Field label="Units in stock" htmlFor="units-in-stock">
<FormatInput.Numeric
id="units-in-stock"
placeholder="0"
thousandSeparator
decimalScale={0}
allowNegative={false}
/>
</Form.Field>
</div>
);
}Affix
Preview
Dark
Value: — — the $ became part of the text
kgValue: — — the icon and “kg” are decoration only
import { useState } from 'react';
import { PiTag } from 'react-icons/pi'
import FormatInput from '@/components/composites/FormatInput';
export default function AffixDemo() {
const [amount, setAmount] = useState<number>();
const [weight, setWeight] = useState<number>();
return (
<div className="mx-auto flex w-full max-w-md flex-col gap-4">
<div className="flex flex-col gap-2">
<FormatInput.Numeric
placeholder="0.00"
prefix="$"
thousandSeparator
decimalScale={2}
onValueChange={(values) => setAmount(values.floatValue)}
/>
<span className="text-xs text-muted-foreground">
Value: {amount ?? '—'} — the $ became part of the text
</span>
</div>
<div className="flex flex-col gap-2">
<FormatInput.Numeric
placeholder="0"
inputPrefix={<PiTag className="text-base text-muted-foreground" />}
inputSuffix="kg"
onValueChange={(values) => setWeight(values.floatValue)}
/>
<span className="text-xs text-muted-foreground">
Value: {weight ?? '—'} — the icon and “kg” are decoration only
</span>
</div>
</div>
);
}Stepper
Preview
Dark
import { useState } from 'react';
import FormatInput from '@/components/composites/FormatInput';
export default function StepperDemo() {
const [seats, setSeats] = useState(1);
return (
<div className="mx-auto w-full max-w-md">
<FormatInput.Numeric
stepper
min={1}
max={20}
step={1}
value={seats}
onValueChange={(values) => setSeats(values.floatValue ?? 1)}
/>
</div>
);
}Pattern
Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
import Form from '@/components/ui/Form';
export default function PatternDemo() {
return (
<div className="mx-auto w-full max-w-md">
<Form.Field label="Phone number" htmlFor="phone-number">
<FormatInput.Pattern
id="phone-number"
format="(###) ###-####"
mask="_"
placeholder="(___) ___-____"
/>
</Form.Field>
<Form.Field label="ZIP + 4" htmlFor="zip-plus-4">
<FormatInput.Pattern
id="zip-plus-4"
format="#####-####"
mask="_"
placeholder="_____-____"
/>
</Form.Field>
</div>
);
}Custom Format
Preview
Dark
import FormatInput from '@/components/composites/FormatInput';
function formatLicenseKey(value: string) {
const clean = value
.toUpperCase()
.replace(/[^A-Z0-9]/g, '')
.slice(0, 16);
return clean.match(/.{1,4}/g)?.join('-') ?? clean;
}
function stripLicenseKey(value: string) {
return value.replace(/[^A-Za-z0-9]/g, '');
}
export default function CustomFormatDemo() {
return (
<div className="mx-auto w-full max-w-md">
<FormatInput.Custom
placeholder="XXXX-XXXX-XXXX-XXXX"
format={formatLicenseKey}
removeFormatting={stripLicenseKey}
/>
</div>
);
}Controlled
Preview
Dark
Raw value: 1200
import { useState } from 'react';
import Button from '@/components/ui/Button';
import FormatInput from '@/components/composites/FormatInput';
export default function ControlledDemo() {
const [value, setValue] = useState(1200);
return (
<div className="mx-auto w-full max-w-md space-y-2">
<FormatInput.Numeric
prefix="$"
thousandSeparator
decimalScale={2}
fixedDecimalScale
value={value}
onValueChange={(values) => setValue(values.floatValue ?? 0)}
/>
<div className="flex items-center justify-between">
<div className="text-muted-foreground">Raw value: {value}</div>
<Button onClick={() => setValue(0)}>Reset</Button>
</div>
</div>
);
}API
FormatInput.Numeric, FormatInput.Pattern, and FormatInput.Custom each wrap a react-number-format primitive over the shared Input surface. All three take inputPrefix/inputSuffix instead of Input's own prefix/suffix, because Numeric and Custom already use prefix/suffix for text that's baked into the formatted value itself.
FormatInput.Numeric
| Prop | Description | Type | Default |
|---|---|---|---|
value | Controlled numeric value. | number | string | null | — |
defaultValue | Initial value for uncontrolled usage. | number | string | null | — |
onValueChange | Fires with the parsed float, raw digits, and formatted string whenever the value changes. | (values: NumberFormatValues, sourceInfo: SourceInfo) => void | — |
thousandSeparator | Character used to group digits, or true for a comma. | boolean | string | — |
decimalSeparator | Character shown before the decimal digits. | string | '.' |
decimalScale | Number of digits kept after the decimal separator. | number | — |
fixedDecimalScale | Pads with trailing zeros to always show decimalScale digits. | boolean | false |
allowNegative | Allows a leading minus sign. | boolean | true |
allowLeadingZeros | Keeps leading zeros instead of stripping them. | boolean | false |
thousandsGroupStyle | Digit grouping convention. | 'thousand' | 'lakh' | 'wan' | 'none' | 'thousand' |
prefix | Text baked into the formatted value, before the digits (for example '$'). | string | — |
suffix | Text baked into the formatted value, after the digits (for example '%'). | string | — |
stepper | Replaces the suffix slot with built-in increment/decrement buttons. Requires controlled value + onValueChange; the buttons have no effect with only defaultValue. | boolean | false |
min | Lowest value the stepper buttons will reach. | number | 0 |
max | Highest value the stepper buttons will reach. | number | Infinity |
step | Amount each stepper click adds or subtracts. | number | 1 |
inputPrefix | Decorative content in Input's prefix slot; not part of the value. | string | ReactNode | — |
inputSuffix | Decorative content in Input's suffix slot; not part of the value. Ignored when stepper is true. | string | ReactNode | — |
size | Control size. | ControlSize | ConfigProvider controlSize |
disabled | Prevents editing and applies disabled control styling. | boolean | — |
...rest | Remaining NumericFormat props from react-number-format, plus native input attributes. | NumericFormatProps | — |
FormatInput.Pattern
| Prop | Description | Type | Default |
|---|---|---|---|
format | Pattern string where each patternChar is a fillable digit slot (for example '(###) ###-####'). | string | — |
mask | Placeholder character shown in empty digit slots, or one character per slot. | string | string[] | — |
patternChar | Character in format that marks a fillable slot. | string | '#' |
allowEmptyFormatting | Shows the literal pattern characters even before any digit is entered. | boolean | false |
value | Controlled value. | string | number | null | — |
defaultValue | Initial value for uncontrolled usage. | string | number | null | — |
onValueChange | Fires with the parsed digits and formatted string whenever the value changes. | (values: NumberFormatValues, sourceInfo: SourceInfo) => void | — |
inputPrefix | Decorative content in Input's prefix slot. | string | ReactNode | — |
inputSuffix | Decorative content in Input's suffix slot. | string | ReactNode | — |
size | Control size. | ControlSize | ConfigProvider controlSize |
disabled | Prevents editing and applies disabled control styling. | boolean | — |
...rest | Remaining PatternFormat props from react-number-format, plus native input attributes. | PatternFormatProps | — |
FormatInput.Custom
| Prop | Description | Type | Default |
|---|---|---|---|
format | Transforms raw characters into the displayed string. | (value: string) => string | identity |
removeFormatting | Strips a formatted string back to raw characters, typically while editing. | (value: string, changeMeta?: ChangeMeta) => string | digits-only |
getCaretBoundary | Marks which caret positions are safe to land on inside the formatted string. | (formattedValue: string) => boolean[] | numeric-boundary heuristic |
value | Controlled value. | string | number | null | — |
defaultValue | Initial value for uncontrolled usage. | string | number | null | — |
onValueChange | Fires with the parsed value and formatted string whenever the value changes. | (values: NumberFormatValues, sourceInfo: SourceInfo) => void | — |
inputPrefix | Decorative content in Input's prefix slot. | string | ReactNode | — |
inputSuffix | Decorative content in Input's suffix slot. | string | ReactNode | — |
size | Control size. | ControlSize | ConfigProvider controlSize |
disabled | Prevents editing and applies disabled control styling. | boolean | — |
...rest | Remaining NumberFormatBase props from react-number-format, plus native input attributes. | NumberFormatBaseProps | — |
NumericFormatProps, PatternFormatProps, and NumberFormatBaseProps are react-number-format's own types; see the library's docs for its full option list.
Types
type NumberFormatValues = {
floatValue: number | undefined;
formattedValue: string;
value: string;
};
type SourceInfo = {
event?: SyntheticEvent<HTMLInputElement>;
source: 'event' | 'prop';
};
type ChangeMeta = {
from: { start: number; end: number };
to: { start: number; end: number };
lastValue: string;
};
type ControlSize = 'sm' | 'md' | 'lg';