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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Always normalize input to raw digits before applying formatting so punctuation never compounds.
- 2Reformatting an input field shifts characters, so adjust the caret by the change in length to avoid jumping the cursor.
- 3Intercepting Backspace on separator characters prevents users from getting stuck deleting punctuation they didn't type.
Related explainers
javascript
async function uploadInBatches(records, uploadFn, { batchSize = 100, concurrency = 3 } = {}) { const batches = []; for (let i = 0; i < records.length; i += batchSize) { batches.push(records.slice(i, i + batchSize));
Uploading records with bounded concurrency
concurrency
worker-pool
async-await
Advanced
8 steps
javascript
import { NextResponse } from 'next/server'; const locales = ['en', 'fr', 'de', 'es']; const defaultLocale = 'en';
Locale routing with Next.js middleware
middleware
i18n
content-negotiation
Intermediate
10 steps
javascript
const express = require('express'); const cookieParser = require('cookie-parser'); const router = express.Router();
Remember-me login with signed cookies in Express
authentication
signed-cookies
sessions
Intermediate
9 steps
javascript
const TOKEN_SPECS = [ ["comment", /^\/\/[^\n]*|^\/\*[\s\S]*?\*\//], ["string", /^"(?:\\.|[^"\\])*"|^'(?:\\.|[^'\\])*'|^`(?:\\.|[^`\\])*`/], ["number", /^0[xX][\da-fA-F]+|^\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/],
Building a syntax highlighter tokenizer
tokenizer
regular-expressions
lexing
Intermediate
8 steps
javascript
export async function compressImage(file, { maxWidth = 1600, maxHeight = 1600, quality = 0.8, mimeType = 'image/jpeg' } = {}) { const bitmap = await createImageBitmap(file); let { width, height } = bitmap;
Compressing images in the browser with canvas
canvas
image-processing
promises
Intermediate
7 steps
typescript
type MatchSegment = { text: string; matched: boolean; };
Splitting text into highlighted match segments
regex
string-matching
text-highlighting
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-live-phone-number-input-mask-explained-javascript-c192/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.