Skip to content

Repository files navigation

馃ジ use-mask-input

Input masks for React and Vue 3. Works with React Hook Form, TanStack Form, vee-validate, Ant Design, and plain inputs.

npm npm downloads bundle size codecov

ko-fi


Documentation 路 API Reference 路 TanStack Form 路 Sponsor

This README covers the common cases. The full guides, every option and the complete API are on the documentation site.

Install

npm install use-mask-input

Usage

import { useMaskInput } from 'use-mask-input';

function PhoneInput() {
  const ref = useMaskInput({ mask: '(99) 99999-9999' });
  return <input ref={ref} />;
}

Mask syntax

Token Accepts Example
9 A digit '999.999.999-99'
a A letter 'aa-9999'
A A letter, uppercased 'AAA-9999'
* A letter or a digit '***-****'
[ ] An optional part '99 [9]9999-9999'
{n}, {n,m} The previous token, n times or n to m times '9{1,5}'
An array Whichever mask fits the input ['(99) 9999-9999', '(99) 99999-9999']

Each one has a guide on the site: static, optional, dynamic, alternator and preprocessing masks.

Numbers and currency

The numeric aliases (numeric, integer, decimal, currency, brl-currency, percentage) format while the user types. 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
}

The unmasked value keeps the radix point you chose ('1234,56'). For a real number, add autoUnmask and unmaskAsNumber. The input below shows R$ 1.234,56 while input.value is the number 1234.56:

const ref = useMaskInput({
  mask: 'brl-currency',
  options: { autoUnmask: true, unmaskAsNumber: true },
});

To follow a locale, read the separators from Intl.NumberFormat:

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; // ','

function PriceInput() {
  const ref = useMaskInput({ mask: 'decimal', options: { groupSeparator, radixPoint } });
  return <input ref={ref} />;
}

More numeric examples, including min/max and percentages, are in the alias guide.

With React Hook Form

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>
  );
}

With TanStack 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>
  );
}

With Ant Design

import { Input } from 'antd';
import { useMaskInputAntd } from 'use-mask-input/antd';

function EmailInput() {
  const ref = useMaskInputAntd({ mask: 'email' });
  return <Input ref={ref} />;
}

Vue 3

Everything above is React. Vue 3 has its own entry point:

import { vMaskInput, useMaskInput } from 'use-mask-input/vue';

The directive

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).

With v-model

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.

With vee-validate

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>

With component libraries

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'" />

The composable

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>

Vue caveats

  • 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, use v-model with autoUnmask: true.
  • noValuePatching: true is unsupported. It disables the property accessor that the whole v-model integration 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.

APIs

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.
isValidWithMask Function. Whether a value, masked or raw, is a complete valid entry for a mask. For schema validators.
getUnmaskedValue Function. Reads the unmasked value off an element, e.g. event.target in onChange.
isMaskComplete Function. Whether the mask on an element is fully filled.
vMaskInput Vue directive. use-mask-input/vue. The Vue default choice.
useMaskInput (Vue) Composable. use-mask-input/vue. Returns { maskRef, unmaskedValue, isComplete }.

Every hook and with* helper also carries unmaskedValue() and isComplete() on what it returns:

const cpf = useMaskInput({ mask: 'cpf' });
<input ref={cpf} onChange={(e) => console.log(getUnmaskedValue(e.target), cpf.isComplete())} />

The formatters need no mounted element, so they also work in table cells and schema validators:

import { formatWithMask, unformatWithMask, isValidWithMask } from 'use-mask-input';

formatWithMask('12345678901', 'cpf'); // '123.456.789-01'
unformatWithMask('123.456.789-01', 'cpf'); // '12345678901'
isValidWithMask('123.456', 'cpf'); // false
formatWithMask('1234,5', 'brl-currency'); // 'R$ 1.234,50'

Signatures and every option are in the API Reference.

Built-in Aliases

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

Examples and options for each alias are in the alias guide.

Works With

  • 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

License

MIT

About

馃ジ A React/Vue Hook for building elegant input masks. Compatible with React Hook Form and Vee Validate.

Topics

Resources

Security policy

Stars

682 stars

Watchers

6 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages