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

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.

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