# Input masks that change a typed or pasted number

Source: https://easeweb.dev/learn/input-masks
Topics: Forms > Phone and card fields take a number however it is typed or pasted (best practice; related to WCAG 3.3.2)
The fix: Either take the number as it is typed, with no mask: a plain field with an example in its hint, and a check on submit that keeps the digits, whatever went between them, and drops a leading country code 1. Or, to keep the grouped look, write the number back as (212) 555-0147 once the field is left, never while it is being typed.
Test with: keyboard, touch, autofill, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html https://www.w3.org/WAI/WCAG22/Techniques/general/G89 https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html https://design-system.service.gov.uk/patterns/telephone-numbers/ https://www.filamentgroup.com/lab/politespace https://github.com/RobinHerbots/Inputmask/issues/981 https://github.com/uNmAnNeR/imaskjs/issues/800 https://github.com/nosir/cleave.js/issues/193

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop's checkout asks for a phone number, so the courier can text when a parcel is on its way. The field is drawn with the format inside it, (___) ___-____, so people can see what to type, and as they type, the digits fill the blanks. The developer builds it with one line of a popular mask library, or with a few lines of their own that add the brackets and the dash as each digit goes in.

A customer copies their number from their contacts, where it is saved as +1 212 555 0147, and pastes it. The field shows (121) 255-5014. It looks like a phone number, so they press Continue, and the texts go to someone else. The mask has ten places, and it fills them with the first ten digits it is given: the country code 1 becomes the start of the area code, and the last 7 is dropped. Typing +1 first does the same: the + is thrown away without a word, and the 1 takes the first place. We tried the mask in Chromium with Inputmask 5.0, IMask 7.6, Maska 3.2 and the hand-written version below, in October 2026, and each turned the pasted number into (121) 255-5014. IMask's tracker has the same kind of report for its +7 mask: a number pasted with a leading 8 comes out wrong.

The two ways it gets built fail in two more ways:

- With the library, the pattern is the field's value as soon as it takes focus. A screen reader reads the brackets and blanks as what the field holds; an Inputmask user reported it read out as "underline underline underline…".
- With the hand-written version, every change rebuilds the value, and the caret jumps to the end. Someone who notices a wrong fourth digit, deletes it and types the right one finds it added last: (212) 550-1479.

## Why it fails

No success criterion fails here, and this guide is a best practice. Success Criterion 3.3.2 Labels or Instructions asks for labels or instructions where content needs input, and its Understanding document says they may give the expected format (technique G89, the format with an example). Both Befores pass it: the field has a label, and the library's pattern even shows the format. Nothing in WCAG asks a field to take a number in more than one form.

The advice comes from design systems and testing. GOV.UK's telephone number pattern says to avoid input masking, because it makes it harder for people to type a number the way they prefer and to copy one out from somewhere else and check it, and to allow spaces, hyphens, dashes and brackets, and country and area codes. Filament Group found the blanks were "little more than aural garbage" read aloud on focus, and that a script rewriting the value as people type makes screen readers read out fragments of it.

The failure is in how a mask reads what it is given. It knows one format with a fixed number of places, reads each character in turn, drops any its format doesn't take, such as the + and the spaces, and puts each digit in the next place until the places run out. A number written another way doesn't fit, and nothing tells the person what was dropped. The caret is the browser's doing: when a script sets an input's value, HTML moves the caret to the end of the text, so a handler that rebuilds the value on every change takes the caret away from where it was.

One part comes close to a criterion. Success Criterion 3.3.1 Error Identification asks that an input error the page detects is described in text, and its Understanding document says that when a value is too high or too low and the page's code changes it by itself to fall within the allowed range, the error still has to be described. A mask that throws away the + and a digit without a word is near that case. The fixes below throw nothing away, so the question doesn't come up.

## Who is affected

Anyone who doesn't type the number digit by digit in the mask's own form meets this: people who paste it, people whose browser fills it in, and people who write it as it is printed on their card. A number saved in a contacts app, or filled in by the browser, can come with its country code in front, so the paste is not an odd case.

## What automation and AI miss

The field has a label, the page passes axe, and a scan never pastes anything. EaseWeb's audit pastes +1 212 555 0147 into each phone field, and 4242 4242 4242 4242 into each card number field, by script, then puts the value back without sending the form. It lists a field whose digits come out different, and a field whose value is a pattern of blanks, for a person to try. These are leads, not findings, since no criterion fails. Where the caret goes, and what a screen reader reads as each digit goes in, need a person. A code assistant asked for "a phone field formatted as (999) 999-9999" can write either Before.

## Before: a mask library with the pattern in the field

```html
<form>
  <label for="tel">Phone number</label>
  <input id="tel" name="tel" type="tel" autocomplete="tel">
  <button type="submit">Continue</button>
</form>
```

```js
import Inputmask from 'inputmask';

const tel = document.getElementById('tel');

// Brackets, a space and a dash, with a blank for each of the ten digits.
Inputmask('(999) 999-9999').mask(tel);
```

- When the field takes focus, its value becomes the pattern (___) ___-____, and a screen reader reads that as what it holds.
- The mask has ten places. Pasted as +1 212 555 0147, the number fills them with its first ten digits and shows (121) 255-5014; the last 7 is dropped.
- The + is thrown away, and nothing says the number changed.

## Before: a formatter written by hand

```html
<form>
  <label for="tel">Phone number</label>
  <input id="tel" name="tel" type="tel" autocomplete="tel">
  <button type="submit">Continue</button>
</form>
```

```js
const tel = document.getElementById('tel');

// Format the number as it is typed: (212) 555-0147.
tel.addEventListener('input', () => {
  const d = tel.value.replace(/\D/g, '').slice(0, 10);
  tel.value = d.length > 6 ? `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`
    : d.length > 3 ? `(${d.slice(0, 3)}) ${d.slice(3)}`
    : d;
});
```

The same field, with the formatting written out:

- Every change sets the value, which sends the caret to the end. Delete a wrong fourth digit and type the right one, and it goes in last: (212) 550-1479.
- A number pasted with +1 in front keeps its first ten digits, as with the library: (121) 255-5014.
- Anything that isn't a digit is thrown away as it is typed, with no word.

## After: no mask, and the digits checked when the form is sent

```html
<form>
  <label for="tel">Phone number</label>
  <p id="tel-hint">Include the area code, such as (212) 555-0147.</p>
  <input id="tel" name="tel" type="tel" autocomplete="tel"
    aria-describedby="tel-hint tel-error">
  <p id="tel-error"></p>
  <button type="submit">Continue</button>
</form>
```

```js
const form = document.querySelector('form');
const tel = document.getElementById('tel');
const error = document.getElementById('tel-error');

// The ten digits of the number, whatever was typed between them, or null.
function digitsOf(value) {
  let digits = value.replace(/\D/g, '');
  if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1);
  return digits.length === 10 ? digits : null;
}

form.addEventListener('submit', (event) => {
  const digits = digitsOf(tel.value);
  tel.setAttribute('aria-invalid', String(!digits));
  error.textContent = digits ? '' : 'Enter the 10 digits of the phone number, including the area code.';
  if (digits) tel.value = digits;
  else event.preventDefault();
});
```

1. Remove the mask, so the field keeps the number as it is typed or pasted, with any spaces, brackets, dashes or dots.
2. Put an example under the label, tied to the field with `aria-describedby`. It is read with the field, and stays on screen while people type.
3. When the form is sent, keep only the digits, and drop a leading 1 when there are eleven, so +1 212 555 0147, (212) 555-0147 and 212.555.0147 are the same ten digits.
4. Give a value with the wrong number of digits a message that says what to enter, tied to the field.
5. Send the ten digits, and keep them the same way on the server, since a request can reach it without this script.

This fixes both Befores. Nothing changes the field while people type, so the caret stays where they put it and a pasted number goes in whole; it passes 3.3.2, as the Befores did, with the format now in words.

## After: the digits grouped once the field is left

```html
<form>
  <label for="tel">Phone number</label>
  <p id="tel-hint">Include the area code, such as (212) 555-0147.</p>
  <input id="tel" name="tel" type="tel" autocomplete="tel"
    aria-describedby="tel-hint tel-error">
  <p id="tel-error"></p>
  <button type="submit">Continue</button>
</form>
```

```js
const form = document.querySelector('form');
const tel = document.getElementById('tel');
const error = document.getElementById('tel-error');

// The ten digits of the number, whatever was typed between them, or null.
function digitsOf(value) {
  let digits = value.replace(/\D/g, '');
  if (digits.length === 11 && digits.startsWith('1')) digits = digits.slice(1);
  return digits.length === 10 ? digits : null;
}

// Group the number once the field is left, never while it is typed.
tel.addEventListener('change', () => {
  const digits = digitsOf(tel.value);
  if (digits) tel.value = `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
});

form.addEventListener('submit', (event) => {
  const digits = digitsOf(tel.value);
  tel.setAttribute('aria-invalid', String(!digits));
  error.textContent = digits ? '' : 'Enter the 10 digits of the phone number, including the area code.';
  if (digits) tel.value = digits;
  else event.preventDefault();
});
```

1. Remove the handler that formatted the number on every change.
2. Put the same example under the label, tied to the field.
3. When the field is left, and its digits make a whole number, write it back as (212) 555-0147. A value that isn't a whole number stays as it was typed, for the message to explain.
4. Check and send the digits when the form is sent, as in the first fix.

This fixes both Befores, and passes 3.3.2. The field only changes once people have finished with it, so the caret stays put while they type and a pasted number is kept whole. The grouped number is what they see, and what a screen reader reads, when they come back to the field.

## Which option to use

Both take a number however it is typed or pasted. Taking it with no mask is the one to use: it is the same check with less code, and it shows the number as the person wrote it, where GOV.UK's Notify team saw some people confused by a reformatted version of their number. Grouping once the field is left is for a design that wants every number shown the same way, such as a card number in fours; it changes the field once, after people have finished typing, and never moves the caret.

## Implementation decisions

If a mask library has to stay, turning its pattern off takes the blanks out of the value (in Inputmask, `showMaskOnFocus: false` and `showMaskOnHover: false`; in IMask, the default `lazy: true`), but it is not a fix. In our tests, IMask with no pattern shown still turned the pasted +1 212 555 0147 into (121) 255-5014.

Leave `maxlength` off. One sized to the format, such as 14 for (212) 555-0147, cuts a pasted +1 212 555 0147 short in the same way, and GOV.UK's text input guidance warns that a strict length stops people formatting a value their usual way. Check the length of the digits when the form is sent instead.

Keep `type="tel"` and `autocomplete="tel"`. The first brings up a phone keypad on touch screens and takes any character; the second lets the browser fill the number in, which may come with the country code, as the check above expects. Don't use `type="number"`, which is for quantities and doesn't take spaces or brackets.

Card numbers follow the same rule: take the number with or without spaces, `autocomplete="cc-number"` and `inputmode="numeric"`, and group it in fours once the field is left, if the design wants the number as it is printed on the card. See [convert what people type instead of rejecting it](/learn/rules-of-thumb#convert-not-reject). A date mask such as MM/DD/YYYY is the same failure; how to take a typed date is in [date pickers that can't be typed in or used by keyboard](/learn/date-picker).

A shop that takes numbers from more than one country has to keep the + and the country code, and needs a library that knows every country's numbering, such as libphonenumber, run when the form is sent rather than on each key.

## Verify the fix

1. Paste +1 212 555 0147 into the field and send the form. The page sends 2125550147, and shows the number back with those digits.
2. Type the number as (212) 555-0147, as 212.555.0147 and as 212 555 0147, and send each one. Each is taken as the same number.
3. Type the number, move the caret back to the fourth digit, delete it and type another. The caret stays where you put it, and the new digit goes in there.
4. With a screen reader, Tab to the field. Its label and hint are read, and no brackets or blanks are read as its value; type a few digits and listen to them read back as typed.
5. Type nine digits and send the form. The message says to enter the 10 digits, including the area code.
6. Send +1 212 555 0147 to the server without the page's script, such as from a test request. The server keeps the same ten digits, or refuses the value with a message that says what to enter.

Record the OS, browser, assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared phone field or its formatting changes.

## Limits

This is a starting pattern, not a guarantee for every application. The example is a US number with ten digits and an optional country code 1; other countries need other lengths, and an international form keeps the +. A number asked for in three boxes, one for each group, is a different failure, covered in [split date and phone fields with no name for each box](/learn/split-fields). How a message reaches people after a failed submission is covered in [form errors that don't say how to fix them](/learn/accessible-form-errors).

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
