React Input Format & Mask, tiny (≈800b) component to transform any input component into formatted or masked input. Supports number, date, phone, currency, credi
React Input Format & Mask, tiny (≈800b) component to transform any input component into formatted or masked input. Supports number, date, phone, currency, credi
A small, dependency-free React hook and component for building formatted and masked inputs without losing the cursor position.
pnpm add rifm
You can also install it with npm install rifm or yarn add rifm.
RIFM is controlled: keep the formatted value in state, pass it to useRifm, and attach the returned props to your input.
import { useState } from "react";
import { useRifm } from "rifm";
const formatInteger = (value: string) => {
const digits = value.replace(/\D/g, "");
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
export function PriceInput() {
const [value, setValue] = useState("");
const rifm = useRifm({
value,
onChange: setValue,
format: formatInteger,
});
return ;
}
The onChange callback receives the formatted string—not the React change event.
[!IMPORTANT] Use
type="text"with an appropriateinputMode. RIFM does not supporttype="number"ortype="date"because those controls do not expose the selection APIs needed to restore the caret.
If hooks are not convenient, use the Rifm component. It accepts the same options and passes input props to its child function.
import { useState } from "react";
import { Rifm } from "rifm";
export function PriceInput() {
const [value, setValue] = useState("");
return (
{({ value, onChange }) => (
)}
);
}
Because RIFM only supplies value and onChange, the input can be a native element or any component that accepts compatible props.
Both useRifm(options) and `` accept the following options.
value
string
The controlled input value.
onChange
(value: string) => void
Called with the next formatted value.
format
(value: string) => string
Formats the value after every edit.
accept
RegExp
/\d/g
Matches characters whose order RIFM tracks when restoring the caret. Use a global regular expression.
replace
(value: string) => string
—
Post-processes the formatted value while preserving the caret, for example to enforce uppercase or replace text.
append
(value: string) => string
—
Post-processes insertions made at the end; useful when a formatter needs to append a separator.
mask
boolean
—
Enables replacement-style mask editing and caret movement across mask separators.
children
(props) => ReactNode
—
Required by `` only. Receives { value, onChange }.
useRifm returns the same object passed to the Rifm child function:
{
value: string;
onChange: React.ChangeEventHandler;
}
Pass both properties to the underlying input.
Import createNumberFormatter from rifm/number. With no options, it uses the runtime's default locale, groups thousands, accepts positive numbers, and preserves any number of fractional digits:
import { createNumberFormatter } from "rifm/number";
const number = createNumberFormatter();
const rifm = useRifm({
value,
onChange: setValue,
...number,
});
The returned { format, accept } object can be spread into either useRifm or ``. Formatting is string-based, so editable states such as 1. and 1.20 are preserved and large integers do not lose precision.
The examples below use the en-US locale unless another locale is specified.
locales
Runtime default
Locale or locale fallback list used by Intl.NumberFormat
{ locales: "de-DE" }: 12345,6 → 12.345,6
useGrouping
true
Enables locale-specific integer grouping
{ useGrouping: false }: 1234.5 → 1234.5
allowNegative
false
Preserves a minus sign when present
{ allowNegative: true }: -1234.5 → -1,234.5
minimumFractionDigits
0
Pads the fractional part with zeroes
{ minimumFractionDigits: 2 }: 12.5 → 12.50
maximumFractionDigits
Unlimited
Truncates the fractional part to this length
{ maximumFractionDigits: 2 }: 12.345 → 12.34
Options can be combined:
const euro = createNumberFormatter({
locales: "de-DE",
allowNegative: true,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
euro.format("-12345,6");
// "-12.345,60"
Fraction digit limits must be non-negative integers, and the minimum must not exceed the maximum.
RIFM restores the caret by tracking the characters matched by accept. The formatter may insert, remove, or move separators, but it should preserve the order of those accepted characters.
For example, with digits as accepted characters:
Before typing: 1,234|67
Type "5": 1,2345|67
Formatted: 1,234,5|67
The commas move, but the digits before the caret remain in the same order, so RIFM can find the correct position. Set accept when formatting something other than digits:
const rifm = useRifm({
value,
onChange: setValue,
format: (value) => value,
replace: (value) => value.replace(/[^a-z ]/gi, "").toUpperCase(),
accept: /[a-z ]/gi,
});
Use replace for transformations such as case enforcement that change accepted characters themselves.
The hook and component can infer common input types. For an explicit custom element or component event type, provide the generic parameter:
const inputProps = useRifm({
value,
onChange: setValue,
format: formatInteger,
});
pnpm install
pnpm test
pnpm run build
The test suite includes TypeScript, unit, and browser tests.
No open issues yet, or sync has not completed.