A phone field masked as (___) ___-____ rejects every number outside North America, and a regex like ^\d{10,11}$ rejects the same UK number written as +44 7700 900123, 07700 900123 or 07700-900-123 — while happily accepting 0000000000.

Phone numbering is a genuinely complex, per-country, frequently updated dataset, which is why this field should use a library built on it — typically libphonenumber-js, a JavaScript port of Google’s libphonenumber metadata. This page, part of input masking and formatting, parses whatever users type, validates against real numbering plans, stores numbers in E.164, and presents them nicely — without a fixed mask.


Context and prerequisites

Concepts the library handles:

  • Default country. 07700 900123 is only meaningful with a country. Parse with the user’s likely country (from locale, address or a selector); numbers typed with + and a country code override it.
  • E.164 — the canonical international format: +447700900123. Store and compare this; it is unambiguous.
  • Validity levels. Possible (right length for the country) versus valid (matches the numbering plan’s patterns). Validity data changes as countries add ranges, so keep the library updated.
  • Number type. Mobile, fixed line, toll-free, premium — useful when the field must receive SMS.
  • Formatting. National (07700 900123), international (+44 7700 900123) and an as-you-type formatter.

Metadata size is the main trade-off: full metadata is large; libphonenumber-js offers min metadata (validity by length only), max (full patterns, larger) and custom builds for the countries you serve.

One number, many ways to type it The inputs 07700 900123, 07700-900-123, +44 7700 900123, 0044 7700 900123 and +44 (0) 7700 900123 all parse, with default country GB, to the same E.164 value plus 447700900123. The input 0000 000000 parses but is not valid because no numbering range matches. Typed Default country E.164 Valid 07700 900123 GB +447700900123 yes 07700-900-123 GB +447700900123 yes +44 7700 900123 (ignored) +447700900123 yes +44 (0) 7700 900123 (ignored) +447700900123 yes 0000 000000 GB — no

The core pattern: parse with a default country, validate, store E.164

import { parsePhoneNumberFromString, AsYouType, type CountryCode } from "libphonenumber-js/max";

export type PhoneResult =
  | { ok: true; e164: string; national: string; international: string; country?: CountryCode; type?: string }
  | { ok: false; message: string };

export function checkPhone(raw: string, defaultCountry: CountryCode, opts: { requireMobile?: boolean } = {}): PhoneResult {
  const text = raw.trim();
  if (!text) return { ok: false, message: "Enter a phone number." };

  // Extensions: accept "ext. 123", "x123", "#123" — the library recognises common forms.
  const phone = parsePhoneNumberFromString(text, defaultCountry);
  if (!phone) {
    return { ok: false, message: "Enter a phone number, like 07700 900123 or +44 7700 900123." };
  }
  if (!phone.isPossible()) {
    return { ok: false, message: "This phone number is too short or too long." };
  }
  if (!phone.isValid()) {
    return { ok: false, message: "Enter a valid phone number. Check the area code and number." };
  }
  const type = phone.getType();   // "MOBILE", "FIXED_LINE", "FIXED_LINE_OR_MOBILE", ...
  if (opts.requireMobile && type && !["MOBILE", "FIXED_LINE_OR_MOBILE"].includes(type)) {
    return { ok: false, message: "Enter a mobile number so we can send you a text." };
  }
  return {
    ok: true,
    e164: phone.number,                           // "+447700900123" — store this
    national: phone.formatNational(),             // "07700 900123"
    international: phone.formatInternational(),   // "+44 7700 900123"
    country: phone.country,
    type,
  };
}

// Optional light formatting while typing, preserving user intent.
export function formatAsYouType(text: string, defaultCountry: CountryCode) {
  return new AsYouType(defaultCountry).input(text);
}
<label for="phone">Mobile number</label>
<p id="phone-hint">We'll text a code to this number. Include the country code if it's not a UK number.</p>
<input id="phone" name="phone" type="tel" inputmode="tel" autocomplete="tel"
       aria-describedby="phone-hint" dir="ltr">

Step-by-step walkthrough

  1. Use type="tel" and autocomplete="tel". Mobile users get the phone keypad; autofill offers saved numbers — see autocomplete tokens for autofill-friendly forms.
  2. Choose a default country deliberately. From the user’s address country, account locale, or a country selector; not just navigator.language, which reflects language, not location.
  3. Parse leniently. Accept spaces, dashes, brackets, dots and +; let the library strip them. Never block characters in the input.
  4. Validate on blur. Check possible, then valid, then (if needed) type. Show specific messages; the library tells you which check failed.
  5. Store E.164. It is the only representation that compares correctly across formats and countries.
  6. Display in the user’s context. National format for numbers in the user’s country, international for others. Format on blur, or as you type with AsYouType if you apply caret mapping, per preserving caret position in masked inputs.

Why the only real validation is a message that arrives

A number can be valid by every numbering-plan rule and still not belong to the user — mistyped by one digit into someone else’s valid number, or disconnected last month. Library validation catches malformed and impossible numbers quickly, which is its job. When the number matters (two-factor authentication, delivery updates), confirm it by sending a one-time code, and design the confirmation step so users can easily correct the number if the code does not arrive. The code-entry field has its own accessibility and autofill considerations, covered in one-time code inputs: keyboard, paste and autofill.

Which message for a phone number If the field is empty, say enter a phone number. If it cannot be parsed at all, show an example in national and international format. If it parses but has an impossible length, say it is too short or too long. If it has a possible length but no valid numbering range, ask the user to check the area code and number. If a mobile is required and the type is fixed line, ask for a mobile so a text can be sent. Otherwise the number is accepted and stored in E.164. Parses at all? "Enter a phone number, like 07700 900123…" fails passes Possible length? "This phone number is too short or too long." fails passes Valid for the numbering plan? "Check the area code and number." fails passes Mobile required but fixed line? "Enter a mobile number so we can text you." fails passes Accept; store E.164 +447700900123

Failure modes and edge cases

1. The trunk prefix in international format

+44 (0) 7700 900123 includes the UK trunk prefix 0 in brackets — common in business signatures and incorrect when dialling internationally. The library handles it; a hand-written parser usually produces an invalid number.

2. Metadata size

libphonenumber-js/max includes full validation patterns and is considerably larger than /min. If you serve a known set of countries, generate custom metadata; if bundle size is critical, use /min on the client for length checks and full validation on the server.

3. Country selectors

A country dropdown next to the field is helpful for international audiences, but users often paste numbers with + that disagree with the selected country. Let the + prefix win and update the selector, rather than rejecting the number.

4. Extensions

Business numbers may include extensions. Store the extension separately (phone.ext) or in the RFC 3966 URI form, and do not strip it silently.

5. Right-to-left pages

Phone numbers are left-to-right strings. In RTL layouts, set dir="ltr" on the input so digits and + do not render in confusing order.

What to store, what to show Store the E.164 form, such as plus 447700900123, because it compares correctly and can be dialled from anywhere. Show numbers in the user's own country in national format, such as 07700 900123. Show numbers from other countries in international format, such as plus 44 7700 900123, so the country code is visible. Store: E.164 +447700900123 Unambiguous; compares correctly. Show (same country) 07700 900123 National format. Show (other country) +44 7700 900123 International format.

Verification checklist


Frequently Asked Questions

Can I validate phone numbers with a regex?

Only very loosely — for example “a + or digit followed by 7 to 15 digits and common separators”. That catches typing mistakes but not invalid ranges, and it cannot normalise to E.164. Use a regex as a quick pre-check at most.

Should the field be required?

Only if you genuinely need it. Phone numbers are sensitive personal data; ask for them when there is a clear purpose and say what it is in the hint. If contact can be by phone or email, see requiring at least one of several fields.

Does the server need the same library?

Yes, or its equivalent in the server language (libphonenumber exists for Java, C++, Python and others). The server should re-parse and store E.164 itself rather than trusting the client’s normalised value.


Related

← Input Masking and Formatting