Input masks for React and Vue 3. Works with React Hook Form, TanStack Form, vee-validate, Ant Design, and plain inputs.
Documentation 路 API Reference 路 TanStack Form 路 Sponsor
npm install use-mask-inputimport { useMaskInput } from 'use-mask-input';
function PhoneInput() {
const ref = useMaskInput({ mask: '(99) 99999-9999' });
return <input ref={ref} />;
}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>
);
}import { useForm } from '@tanstack/react-form';
import { useTanStackFormMask } from 'use-mask-input';
function MyForm() {
const maskField = useTanStackFormMask();
const form = useForm({
defaultValues: {
phone: '',
},
onSubmit: async ({ value }) => {
console.log(value);
},
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="phone">
{(field) => {
const inputProps = maskField(
'(99) 99999-9999',
{
name: field.name,
value: field.state.value,
onBlur: field.handleBlur,
onChange: (event) => field.handleChange(event.target.value),
},
);
return <input {...inputProps} placeholder="(00) 00000-0000" />;
}}
</form.Field>
</form>
);
}import { Input } from 'antd';
import { useMaskInputAntd } from 'use-mask-input/antd';
function EmailInput() {
const ref = useMaskInputAntd({ mask: 'email' });
return <Input ref={ref} />;
}Everything above is React. Vue 3 has its own entry point:
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.
<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>Outside <script setup>, register it yourself: app.directive('mask-input', vMaskInput).
v-model works with no extra code. Add autoUnmask and the bound value is the raw one, while the input keeps showing the mask:
<input v-model="cpf" v-mask-input="{ mask: 'cpf', options: { autoUnmask: true } }" />
<!-- displays 123.456.789-01, and cpf === '12345678901' -->Without autoUnmask, v-model receives the masked string. Directive order does not matter: v-model before or after v-mask-input behaves identically.
Binding null turns masking off and removes any mask already applied, so v-mask-input="enabled ? 'cpf' : null" works as a toggle.
No helper, no wrapper. useField plus v-model plus the directive is the whole integration, and handleSubmit receives the unmasked value:
<script setup>
import { useField, useForm } from 'vee-validate';
import { vMaskInput } from 'use-mask-input/vue';
const { handleSubmit } = useForm();
const { value, errorMessage } = useField('cpf', (v) => v?.length === 11 || 'Invalid CPF');
const onSubmit = handleSubmit((values) => console.log(values)); // { cpf: '12345678901' }
</script>
<template>
<form @submit="onSubmit">
<input v-model="value" v-mask-input="{ mask: 'cpf', options: { autoUnmask: true } }" />
<span>{{ errorMessage }}</span>
</form>
</template>Put the directive on the component. Vue applies it to the root element and the mask finds the inner input, so PrimeVue, Element Plus and Ant Design Vue work as-is:
<InputText v-mask-input="'cpf'" />For imperative reads, or when you want the raw value without autoUnmask:
<script setup>
import { useMaskInput } from 'use-mask-input/vue';
const { maskRef, unmaskedValue } = useMaskInput('cpf');
const submit = () => console.log(unmaskedValue());
</script>
<template>
<input :ref="maskRef" />
</template>unmaskedValue()is not reactive.{{ unmaskedValue() }}renders once and never updates, because reading the DOM registers no reactive dependency. Use it in event handlers and imperative code. For a value the template tracks, usev-modelwithautoUnmask: true.noValuePatching: trueis unsupported. It disables the property accessor that the wholev-modelintegration relies on.- Replace the options object, don't mutate it. Options are compared shallowly, so an in-place mutation may not re-apply the mask.
There is deliberately no <MaskInput> component and no vee-validate helper, because v-model plus the directive already covers both.
| API | Description |
|---|---|
useMaskInput |
Hook. Returns a ref callback. Default choice. |
useHookFormMask |
Hook. Wraps React Hook Form's register. |
useTanStackFormMask |
Hook. Adds mask to TanStack Form field input props. |
withMask |
Function. Ref callback. Requires React.memo. |
withHookFormMask |
Function. Mask for registered fields. Requires React.memo. |
withTanStackFormMask |
Function. Mask for TanStack input props. Requires React.memo. |
useMaskInputAntd |
Hook. useMaskInput for Ant Design. |
useHookFormMaskAntd |
Hook. useHookFormMask for Ant Design. |
formatWithMask |
Function. Formats a raw value using a mask, without a mounted element. |
unformatWithMask |
Function. Removes the mask from a formatted value, without a mounted element. |
vMaskInput |
Vue directive. use-mask-input/vue. The Vue default choice. |
useMaskInput (Vue) |
Composable. use-mask-input/vue. Returns { maskRef, unmaskedValue }. |
cpf 路 cnpj 路 cep 路 phone-br 路 date-br 路 plate-br 路 br-bank-account 路 br-bank-agency 路 currency 路 brl-currency 路 credit-card 路 time 路 datetime 路 email 路 numeric 路 decimal 路 integer 路 percentage 路 url 路 ip 路 mac 路 ssn
- TanStack Form (
useTanStackFormMask,withTanStackFormMask). See the TanStack Form guide. - React Hook Form
- Ant Design (
use-mask-input/antd) - React Final Form
- Next.js / SSR
- Vue 3 (
use-mask-input/vue) - vee-validate (no adapter needed)
- Vue component libraries: PrimeVue, Element Plus, Ant Design Vue
MIT