Phone

Phone number validation, parsing, and formatting — the same helpers PhoneInput uses, without the input.

Quick Preview

Valid number
validatePhoneNumber
true
parsePhoneNumber
+2348012345678
formatPhoneNumber
+234 801 234 5678
formatPhoneNumber · "national"
0801 234 5678
formatPhoneNumber · "rfc3966"
tel:+2348012345678
getPhoneNumberCountry
NG

Usage

A phone number is validated in far more places than it is typed — a schema, a server action, a CSV import, a table cell — and every one of those was otherwise reaching for its own regex. These carry no React and no styling, so a server-only module can import them.

import {
  formatPhoneNumber,
  parsePhoneNumber,
  validatePhoneNumber,
} from "@giddaa-housing/ui/utils/phone";

validatePhoneNumber("+2348012345678");                   // true
parsePhoneNumber("0801 234 5678", { country: "NG" });    // "+2348012345678"
formatPhoneNumber("+2348012345678");                     // "+234 801 234 5678"

One rule runs through all of them: invalid in, empty out. Anything that is not a real, dialable number formats and parses to "" — never null, never undefined, never a half-cleaned version of what you passed. There is one thing to check instead of three.

With a schema

validatePhoneNumber is written to be passed by reference. Both libraries call a validator with a second argument of their own — a refinement context, a test context — and it is built to ignore anything that is not an options object.

// zod
z.object({
  phone: z.string().refine(validatePhoneNumber, "Enter a valid phone number"),
});

// yup
yup.object({
  phone: yup
    .string()
    .test("phone", "Enter a valid phone number", validatePhoneNumber),
});

Empty and null are invalid. Mark the field optional in the schema rather than asking the validator to decide whether a missing number is a problem — that is the schema's business.

To validate against a country, wrap it:

z.string().refine((value) => validatePhoneNumber(value, { country: "NG" }));

Countries

A number written nationally — 0801 234 5678 — is a valid Nigerian number and nothing at all without a country. One that carries its own calling code needs no help.

validatePhoneNumber("0801 234 5678");                 // false
validatePhoneNumber("0801 234 5678", { country: "NG" }); // true

Examples

Validating, parsing, and formatting

Type a number and switch the country to watch each helper respond. Note that an invalid number blanks the parsed and formatted results rather than showing a partial one.

Valid number
validatePhoneNumber
true
parsePhoneNumber
+2348012345678
formatPhoneNumber
+234 801 234 5678
formatPhoneNumber · "national"
0801 234 5678
formatPhoneNumber · "rfc3966"
tel:+2348012345678
getPhoneNumberCountry
NG

Storing and displaying

Parse on the way in, format on the way out. Store E.164 and nothing else, so a number is comparable and dialable wherever it ends up.

const stored = parsePhoneNumber(input, { country });   // "+2348012345678"

<a href={formatPhoneNumber(stored, { format: "rfc3966" })}>
  {formatPhoneNumber(stored)}
</a>;

A country list

getPhoneCountries returns what PhoneInput builds its own picker from, so a country field elsewhere in the same form lists the same countries in the same order.

{getPhoneCountries().map(({ code, name, callingCode }) => (
  <SelectItem key={code} value={code}>
    {name} (+{callingCode})
  </SelectItem>
))}

Props

Prop/APITypeDefaultDescription
validatePhoneNumber(value, options?) => boolean-Whether the value is a number that could be dialed. Checks the number against its country's assigned ranges, not just its digit count.
parsePhoneNumber(value, options?) => string-The E.164 form for storage, or "".
formatPhoneNumber(value, options?) => string-The number written for a reader, or "".
getPhoneNumberCountry(value, options?) => CountryCode | undefined-The country a valid number belongs to.
getPhoneCountries(locale?) => PhoneCountry[]"en"Every country, as { code, name, callingCode }, sorted by display name.
options.countryCountryCode-The country to read a number against when it carries no calling code.
options.format"international" | "national" | "e164" | "rfc3966""international"Only on formatPhoneNumber.

Every helper takes unknown rather than string, so a null from a database or a context object from a validation library is a false or a "" rather than a crash.

Accessibility

  • Display numbers with formatPhoneNumber rather than raw E.164. The grouped form is read out in digestible chunks by a screen reader; an unbroken run of digits is read as one long number.
  • Use format: "rfc3966" for a tel: href so the link is dialable on a phone and announced as a phone number.
  • Do not rely on formatting alone to signal that a number is invalid — pair it with a message, since the empty string these helpers return has nothing to announce.

On this page