use-mask-input: input masks for React and Vue that keep the raw value separate
🥸 A React/Vue Hook for building elegant input masks. Compatible with React Hook Form and Vee Validate.
At a glance
- What is it?
- A hook and a Vue directive that format what the user types as they type it, with a mask syntax that covers digits, letters, optional parts, repetition and arrays of alternatives, and adapters for React Hook Form, TanStack Form, vee-validate and Ant Design. The distinguishing detail is that the masked display and the unmasked value are separate things.
- Who is it for?
- Adopt use-mask-input if you build forms in React or Vue 3 and need formatted input that still submits a clean value, since the masked display and the unmasked value are separated by design and the library ships adapters for the form libraries people actually use. Skip it if you need server side validation as the primary guarantee, because a mask formats what is typed rather than establishing what is valid.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A ref on the input, and a mask string
The whole React API fits in a few lines. You install the package, call a hook with a mask, and put the returned ref on an input:
import { useMaskInput } from 'use-mask-input';
function PhoneInput() {
const ref = useMaskInput({ mask: '(99) 99999-9999' });
return <input ref={ref} />;
}The mask itself is a string that mixes literal characters and token characters, and the readme has a table of the token vocabulary. A 9 accepts a digit, a a accepts a letter, a A accepts a letter uppercased, and a * accepts either. Square brackets make a part optional, curly braces repeat the previous token n times or n to m times, and an array of masks means whichever mask fits the input.
So the mask language is small but has enough structure to describe real formats, and the examples in the table show the intended shapes: a Brazilian CPF style mask of 999.999.999-99, a postal code of aa-9999, and a phone of 99 [9]9999-9999 where the fourth digit is optional.
The part that matters more than the syntax is what happens to the value. A mask is a display format, and the library is careful to keep the display and the underlying value separate. On the React side that is what the autoUnmask and unmaskAsNumber options are for, and it is the reason the library fits into form libraries that validate and submit values rather than strings typed by a person.
The package is MIT licensed, the documentation site is use-mask-input.eduardoborges.dev, and the last push was on 2026-10-09 with the most recent releases at 3.14.2 on 2026-09-12, 3.14.1 on 2026-08-28 and 3.14.0 on 2026-08-26.
The mask language, and the five kinds of mask the docs split out
Rather than one syntax to cover everything, the documentation site splits the masks into five kinds, and the readme links a guide for each: static, optional, dynamic, alternator and preprocessing.
A static mask is the one above, a fixed pattern of tokens and literals. An optional mask uses the square bracket form, so a phone number where the area code has a variable digit count is 99 [9]9999-9999. A dynamic mask is the one where the accepted length depends on what has been typed so far, which is where the array form comes in: an array of masks means whichever mask fits the input. An alternator mask is the array form used explicitly to switch between alternative formats, for example 999-999 or 999-999-999, and a preprocessing mask is the one where the value is transformed before it is masked.
The reason this matters is that the naive version of an input mask library is a regular expression over the raw string, and that approach handles the easy case and breaks on everything else. Optional digits, multiple valid lengths, and letters that must be uppercased all need the mask to carry structure rather than a single pattern, and that structure is what the token vocabulary provides.
The array form is the most interesting of the set, because it changes the problem from validation to formatting. Instead of rejecting a value that does not fit, the mask accepts it and formats it into whichever of the listed shapes it matches. A document number that is nine digits in one region and eleven in another is a case where the user should not be told they are wrong, and the array is how you express that.
Numbers and currency, where the unmasked value is the point
The numeric aliases are the part of the library that goes beyond formatting characters, and they are worth reading closely because they change what your form receives.
The numeric aliases, numeric, integer, decimal, currency, brl-currency and percentage, format while the user types, and you set the separators and the decimal places in options:
function PriceInput() {
const ref = useMaskInput({
mask: 'decimal',
options: { radixPoint: ',', groupSeparator: '.', digits: 2 },
});
return <input ref={ref} />; // typing 1234567,89 shows 1.234.567,89
}And the unmasked value keeps the radix point you chose, so the value behind that field is 1234,56 and not 1.234,56.
For a real number rather than a formatted string, you add autoUnmask and unmaskAsNumber:
const ref = useMaskInput({
mask: 'brl-currency',
options: { autoUnmask: true, unmaskAsNumber: true },
});The readme states the result plainly. The input shows R$ 1.234,56 while input.value is the number 1234.56. That is the whole design goal of the library in one sentence, and it is why the form libraries are supported rather than bolted on afterwards: a validation library wants a number to validate, not a display string with grouping separators in it.
There is also a locale route, which is the part most currency mask libraries get wrong. To follow a locale you read the separators from Intl.NumberFormat rather than hard coding them:
const parts = new Intl.NumberFormat('de-DE').formatToParts(1000.1);
const groupSeparator = parts.find((p) => p.type === 'group')?.value; // '.'
const radixPoint = parts.find((p) => p.type === 'decimal')?.value; // ','More numeric examples, including min and max and percentages, are in the alias guide on the site.
The form library adapters, one per framework
What makes this usable in a real application is that the mask is not bolted on top of a form library, it is integrated with each one.
For React Hook Form the integration is useHookFormMask, which wraps the library's register function and returns a function you call per field with the mask:
import { useForm } from 'react-hook-form';
import { useHookFormMask } from 'use-mask-input';
function MyForm() {
const { register, handleSubmit } = useForm();
const registerWithMask = useHookFormMask(register);
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...registerWithMask('phone', '(99) 99999-9999')} />
<input {...registerWithMask('email', 'email')} />
<button type="submit">Submit</button>
</form>
);
}TanStack Form gets useTanStackFormMask, and here the shape is different because TanStack Form is field based rather than register based. The mask is applied to a field's props inside a form.Field render function, and the value and onChange are passed in explicitly:
const maskField = useTanStackFormMask();Ant Design has its own entry point, use-mask-input/antd, because the Ant Design Input is a controlled component that takes a value prop rather than being an uncontrolled DOM input:
import { Input } from 'antd';
import { useMaskInputAntd } from 'use-mask-input/antd';
function EmailInput() {
const ref = useMaskInputAntd({ mask: 'email' });
return <Input ref={ref} />;
}That per framework entry point pattern is the design decision here, and it is a good one. Rather than one hook with a lot of conditional behaviour, each integration gets its own module that knows how that library manages focus, refs and controlled values.
Vue gets a directive, and the directive order does not matter
Vue 3 has its own entry point, and the integration is a directive rather than a hook.
import { vMaskInput, useMaskInput } from 'use-mask-input/vue';In script setup, importing vMaskInput is enough. Vue resolves a vFoo binding to v-foo, so there is no registration step and no plugin. Outside script setup, you register it yourself with app.directive('mask-input', vMaskInput).
The directive usage covers the mask forms you would expect:
<script setup>
import { vMaskInput } from 'use-mask-input/vue';
</script>
<template>
<input v-mask-input="'cpf'" />
<input v-mask-input="'(99) 99999-9999'" />
<input v-mask-input="['999-999', '999-999-999']" />
<input v-mask-input="{ mask: 'currency', options: { prefix: 'R$ ' } }" />
</template>With v-model, the readme is unusually clear about two things. The v-model works with no extra code, and adding autoUnmask means the bound value is the raw one while the input keeps showing the mask, displaying 123.456.789-01 with cpf equal to 12345678901. Without autoUnmask, v-model receives the masked string. And directive order does not matter, v-model before or after v-mask-input behaves identically.
That last point matters more than it sounds. Vue compiles v-model into an event handler and a prop, and the order of the two directives in a template affects the order in which those hooks run. A library that documents identical behaviour in both orders is one where you do not have to think about it, and where a future Vue change to that ordering would not silently break every form in your application.
Binding null turns masking off and removes any mask already applied, so v-mask-input="enabled ? 'cpf' : null" works as a toggle.
vee-validate, and the absence of a wrapper
The vee-validate integration is the shortest of them, and the readme is emphatic about the reason.
No helper, no wrapper. useField plus v-model plus the directive is the whole integration, and handleSubmit receives the unmasked value.
That is the point of the whole design, stated in the smallest possible example. A library that required a wrapper for every form framework would eventually have a wrapper for every form framework, and each wrapper would be a place where the masked and unmasked values could drift apart. Here the directive is the only moving part, and the unmasked value is what reaches the submit handler.
So the four integrations, React Hook Form, TanStack Form, Ant Design and vee-validate, are not equal in complexity. React Hook Form needs a wrapper around register. TanStack Form needs the field props passed in. Ant Design needs its own entry point because of controlled inputs. And vee-validate needs nothing beyond the directive, because v-model already carries the value and the library only needs to format what is displayed.
The readme also names other libraries it works with, at the top of the file, alongside plain inputs: React Hook Form, TanStack Form, vee-validate, Ant Design, and plain inputs. A plain input is the floor of the design, and the integrations are the variations on it.
Where this library is the wrong choice
There are cases where an input mask is the wrong tool, and one where this particular library is not the right one.
The first is when the mask would be a substitute for validation rather than a formatting aid. A mask constrains what can be typed, which is useful for a phone number or a date, and it is the wrong instrument for a field where the valid set is open, like a free text search or an address line. Client side masks are also a convenience to the user, never a guarantee, because the value can arrive from anywhere. If the server does not validate, a mask on the client changes nothing about the data you store.
The second is a country specific format library. The readme shows a Brazilian CPF mask and a Brazilian Real currency alias, and the library is written by a Brazilian author with a Brazilian phone number in the example. For a codebase that only ever handles one national format, a small purpose built mask, or the browser's own input type attributes for the simple cases, is a smaller dependency.
The third is the rate of change. The release history shows 3.14.0 on 2026-08-26, 3.14.1 on 2026-08-28 and 3.14.2 on 2026-09-12, three releases in a few weeks. The library is being iterated on quickly, which is good for a project finding its shape and a cost for a team that pins versions. The package manifest in the repository root shows a monorepo with a changeset based release process, and the versioning is per package rather than across the monorepo, so a React user and a Vue user can move at different speeds.
The fourth is the React ref model. The hook returns a ref that you attach to the input, and the mask intercepts what the user types. If your form needs a controlled input with the value held in state, the Ant Design entry point exists for that case, and for other controlled components you would be reading a ref based API that does not fit.
And the fifth is simply that masking and unmasking are two different problems. If all you need is a formatted currency field with a number underneath, the autoUnmask and unmaskAsNumber options cover it. If you need the user to be prevented from typing a letter into a numeric field, that is a different library, and a validation rule is usually the better place for it.
Against a native input type, and against a validation schema
Two comparisons make the case for the library's existence.
The first is against the native input type attributes. The browser already offers type="tel", type="email" and type="number", and those give you a mobile keyboard and basic format feedback for free. What they do not give you is a display format. A phone number typed with a numeric keyboard still arrives as the digits the user pressed, and a currency field still shows raw digits. The difference between a native input type and an input mask is that the first constrains the keyboard and the second constrains the shape of what is displayed. A library that only did the first would be redundant; one that does the second is not.
The second is against a validation schema. A schema validates a value once it is complete and tells the user what is wrong. A mask shapes the value while it is being entered and never rejects it. Those are complementary rather than competing, and the reason the form library adapters exist is that the two have to be composed: mask the display, validate the underlying value, submit the unmasked one.
So the honest summary is that use-mask-input is a formatting layer with a clean separation between what the user sees and what the program stores, and its value is highest in the two cases where that separation matters most, which are currency amounts and identity numbers with a fixed shape. Elsewhere, a native input type or a schema rule is less machinery for the same result.
Editorial conclusion
Adopt use-mask-input if you build forms in React or Vue 3 and need formatted input that still submits a clean value, since the masked display and the unmasked value are separated by design and the library ships adapters for the form libraries people actually use. Skip it if you need server side validation as the primary guarantee, because a mask formats what is typed rather than establishing what is valid. Verify first that the mask syntax you need is one of the documented token types, that the value your form library receives is the unmasked one, and that the version you pin matches the form library version you use, since the package releases move quickly.
Frequently asked questions
How do I install and use use-mask-input in React?
Install it with npm install use-mask-input, then call the useMaskInput hook with a mask and attach the returned ref to the input. The simplest example uses useMaskInput({ mask: '(99) 99999-9999' }) on a plain input element.
What mask tokens does use-mask-input support?
A 9 accepts a digit, a a accepts a letter, a A accepts a letter uppercased, a * accepts a letter or a digit, square brackets mark an optional part, curly braces repeat the previous token n times or n to m times, and an array of masks means whichever mask fits the input.
How do I get a numeric value instead of a formatted string from use-mask-input?
Add the autoUnmask and unmaskAsNumber options, for example with the brl-currency mask. The readme states that the input then shows R$ 1.234,56 while input.value is the number 1234.56, and that the unmasked value keeps the radix point you chose.
Does use-mask-input work with React Hook Form and TanStack Form?
Yes. For React Hook Form you use useHookFormMask to wrap register, and for TanStack Form you use useTanStackFormMask and apply the mask to the field props inside a form.Field render function. There is also a dedicated Ant Design entry point at use-mask-input/antd.
How do I use input masks with Vue 3?
Vue has its own entry point at use-mask-input/vue, where importing vMaskInput in script setup is enough because Vue resolves a vFoo binding to v-foo. With v-model, adding autoUnmask gives you the raw bound value while the input keeps showing the mask, and directive order does not matter.
Is use-mask-input actively maintained and what licence does it use?
The repository is not archived and the last push was on 2026-10-09, with recent releases 3.14.2 on 2026-09-12, 3.14.1 on 2026-08-28 and 3.14.0 on 2026-08-26. The licence is MIT, and the documentation site is use-mask-input.eduardoborges.dev.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/eduardoborges-use-mask-input)Was this useful?