MaskInput

Text input that formats what is typed — pattern masks, currency and percentage amounts, and attached affixes.

Quick Preview

Usage

// Pattern mask
<MaskInput preset="date" placeholder="MM/DD/YYYY" />
<MaskInput mask="#### #### ###" placeholder="0000 0000 000" />

// Numeric mask
<MaskInput currency="NGN" locale="en-NG" placeholder="Asking price" />
<MaskInput suffix="%" max={100} placeholder="Equity contribution" />

MaskInput works in one of two modes.

Pattern mode follows a mask (or a named preset). # matches a digit, a a letter, A an upper-cased letter, and * any alphanumeric character. Every other character is a literal that is inserted as the user types, so a literal digit like the 1 in "+1 (###)" stays literal.

Numeric mode formats a free-length number with Intl.NumberFormat — locale grouping, locale decimal separator, an optional currency symbol, and min/max limits. It turns on when you pass numeric, currency, or any numeric-only option (min, max, decimalScale, thousandSeparator) without a mask.

In both modes prefix and suffix are attached to the text rather than parked in the corners of the field, and they stay hidden until there is a value to attach them to.

The caret only ever counts characters the user typed. Home and End stop at the ends of the value rather than the ends of the text, clicking an affix snaps into the value, and backspace or delete pressed on a separator, literal, or affix moves the caret past it instead of deleting through it — so a keypress never removes a character the caret was not sitting next to.

Examples

Default MaskInput

Use this as the starting point for MaskInput. Keep product data, routing, fetching, and authorization logic in the consuming app.

Presets

Named presets cover the most common patterns without spelling out the mask.

Custom mask

Provide a mask string for anything the presets don't cover. onValueChange reports the display value, the raw characters, and the parsed number.

Unmasked value:

Currency

Pass a currency code and the symbol, grouping, and decimal places come from Intl.NumberFormat for the given locale — including whether the symbol leads (₦1,500.00) or trails (1.500,00 €). The field starts empty: the symbol appears with the first digit, not before it. On blur the amount settles at the currency's decimal places.

Submitted value:

<MaskInput currency="NGN" locale="en-NG" placeholder="Asking price" />

Percentage and number ranges

max refuses any keystroke that would take the value beyond it, so a capped percentage can never reach 101 — while a field left uncapped runs past 100 freely. min is not enforced mid-typing (you have to be able to type the 1 of 15); it is applied on blur.

// Refuses the keystroke that would exceed 100
<MaskInput suffix="%" max={100} decimalScale={2} />

// No cap — appreciation can exceed 100%
<MaskInput numeric suffix="%" decimalScale={1} />

Prefix and suffix

Affixes are part of the value's text, not positioned addons, so they hug the number and disappear when the field is emptied. Use InputGroup addons instead when the affix should sit in the corner of the field.

<MaskInput numeric suffix=" sqm" />
<MaskInput prefix="₦" suffix=" / month" decimalScale={0} />

Custom validation

validate runs on every proposed change with the partial value and can reject a keystroke by returning false — here the hour is capped at 23 and the minute at 59.

function isValidTime({ masked }: { masked: string }) {
	const [hour, minute] = masked.split(":");
	if (hour && Number(hour) > 23) return false;
	if (minute && Number(minute) > 59) return false;
	return true;
}

<MaskInput preset="time" validate={isValidTime} placeholder="HH:MM" />

Props

Prop/APITypeDefaultDescription
maskstring-Pattern of tokens and literals. Overrides preset.
preset"date" | "time" | "creditCard" | "cvc" | "phoneNg"-Named mask used when mask is omitted.
tokensRecord<string, MaskToken>built-inOverride or extend the token map.
numericbooleaninferredForce numeric mode. Inferred from currency, min, max, decimalScale, or thousandSeparator.
currencystring-ISO code ("NGN"). Sets the symbol, its side, and the decimal places from Intl.
localestringruntime localeLocale used for grouping, decimal separator, and currency symbol.
prefixstring-Text attached before the value. Hidden while the value is empty.
suffixstring-Text attached after the value. Hidden while the value is empty.
thousandSeparatorbooleantrueGroup the integer part using the locale's separator.
decimalScalenumbercurrency defaultMaximum decimal places. 0 blocks the decimal separator entirely.
fixedDecimalScalebooleantrue with currencyPad out to decimalScale on blur.
allowNegativebooleantrue when min < 0Accept a leading minus sign.
minnumber-Lower bound. Applied on blur.
maxnumber-Upper bound. Refuses keystrokes that would exceed it.
valuestring | number-Controlled value, in the unmasked (.-decimal) form.
defaultValuestring | number-Uncontrolled initial value.
validate(value: MaskedValue) => boolean-Return false to reject a keystroke. Runs on the partial value.
onValueChange(value: MaskedValue) => void-Fires with { masked, unmasked, number }.
formatMaskValue(value, options) => MaskedValue-Standalone formatter, for rendering the same values outside an input.
sizeinput sizeinheritedInput size from input-size context.
classNamestring-Local layout or spacing overrides.
native/root propsReact component props-Passed through to the underlying input primitive.

Accessibility

  • Provide visible labels or accessible names for interactive controls.
  • Preserve the component-provided focus, keyboard, disabled, and invalid-state behavior.
  • Do not communicate state with color alone; include text, icons, or helper copy.
  • Rejected keystrokes are silent, so state the limit in helper text (“Up to 100%”) rather than relying on the field refusing input.

On this page