javascript 37 lines · 7 steps

Building a live phone number input mask

A formatter plus two input listeners turn raw typing into a formatted phone number while keeping the caret sane.

Explained by highlit
1function formatPhoneNumber(value) {
2 const digits = value.replace(/\D/g, '').slice(0, 10);
3 const parts = [];
4 
5 if (digits.length > 0) {
6 parts.push('(' + digits.slice(0, 3));
7 }
8 if (digits.length >= 4) {
9 parts.push(') ' + digits.slice(3, 6));
10 }
11 if (digits.length >= 7) {
12 parts.push('-' + digits.slice(6, 10));
13 }
14 
15 return parts.join('');
16}
17 
18function attachPhoneMask(input) {
19 input.addEventListener('input', (event) => {
20 const el = event.target;
21 const previousLength = el.value.length;
22 const caret = el.selectionStart;
23 
24 el.value = formatPhoneNumber(el.value);
25 
26 const delta = el.value.length - previousLength;
27 const nextCaret = Math.max(0, caret + delta);
28 el.setSelectionRange(nextCaret, nextCaret);
29 });
30 
31 input.addEventListener('keydown', (event) => {
32 if (event.key === 'Backspace' && /[)\s-]$/.test(event.target.value)) {
33 event.preventDefault();
34 event.target.value = event.target.value.replace(/[)\s-]+$/, '');
35 }
36 });
37}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Always normalize input to raw digits before applying formatting so punctuation never compounds.
  2. 2Reformatting an input field shifts characters, so adjust the caret by the change in length to avoid jumping the cursor.
  3. 3Intercepting Backspace on separator characters prevents users from getting stuck deleting punctuation they didn't type.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a live phone number input mask — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code