Nasaq
Components

PhoneInput

Phone number field with a searchable country list (SVG flag, name in English or Arabic, calling code; Gulf and Arab countries first) and a digits input that groups the number as you type. The value is E.164.

PreviewOpen ↗

Code

import { Field, FieldLabel, PhoneInput } from "@fadymondy/nasaq/web";import { useState } from "react";export function Contact() {  const [phone, setPhone] = useState("");  return (    <Field>      <FieldLabel>Mobile number</FieldLabel>      <PhoneInput value={phone} onValueChange={setPhone} />    </Field>  );}

Forms · beta

Live examples and controls: PhoneInput in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/phone-input.json

One bordered control for a phone number. The start edge holds a country trigger (flag and calling code) that opens a searchable list of every country, each with its flag, its name in English or Arabic and its calling code. The rest is the digits field, which groups the number the way it is written in that country (50 123 4567) and shows an example number as the placeholder. The component emits a canonical E.164 string such as +966501234567.

Calling codes, grouping and examples come from libphonenumber-js (about 19 KB gzipped). Flags are SVGs from country-flag-icons, drawn by CountryFlag, so they look the same on Windows, where flag emoji do not render.

When to use

  • Sign-up, checkout and contact forms that need a number a backend can dial.
  • Any place users in the Gulf and Arab world type numbers, since those countries are listed first.

When not to use

  • Checking that a number really exists or can receive SMS: that needs a lookup service. isValidE164 only checks the numbering plan.
  • A one-time code: use OtpInput.
  • A plain text field: use Input.

Import

import { PhoneInput } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { Field, FieldLabel, PhoneInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Contact() {
  const [phone, setPhone] = useState("");
  return (
    <Field>
      <FieldLabel>Mobile number</FieldLabel>
      <PhoneInput value={phone} onValueChange={setPhone} />
    </Field>
  );
}

Value format

value is E.164: a plus, the calling code, then the national digits, no spaces (+966501234567). It is "" when no digits are typed. While the digits are empty the selected country is still kept in the component.

  • A leading 0 in the digits is a trunk prefix and is dropped: 0501234567 with Saudi Arabia selected gives +966501234567. Italy keeps its leading 0.
  • Spaces, dashes and brackets are removed as you type; at most 15 digits in total (E.164 limit).
  • Arabic-Indic (٠١٢) and Persian (۰۱۲) digits are read as 0-9.
  • The field shows the digits grouped for the country (50 123 4567, 416 555 0123); the caret stays after the digit you typed.
  • Typing or pasting a number that starts with + (or a browser autofill) picks the country from the number. Shared codes resolve by number range: +1 416… selects Canada, +1 202… the United States. A bare +1 selects the main country, the United States.
  • Passing a value (or defaultValue) selects its country the same way.

Anatomy

PhoneInput                  InputGroup                       data-slot="phone-input"
├─ country trigger          Base UI Combobox.Trigger         data-slot="phone-input-country"
│  └─ popup                 search + list                    data-slot="phone-input-content"
│     ├─ search input       data-slot="phone-input-search"
│     └─ items              CountryFlag, name, +dial
├─ digits input             InputGroupInput (Field control)  dir="ltr" inputMode="tel" autoComplete="tel"
└─ hidden input             only when `name` is set

API

PhoneInput

PropTypeDefaultDescription
value?stringnoneControlled value, E.164. A local number (0591234567) or 00966… is accepted and kept as the national digits of defaultCountry (or the country the 00 code names) instead of being dropped; onValueChange then reports E.164.
defaultValue?string""Initial E.164 value when uncontrolled.
onValueChange?(value: string, country: PhoneCountry) => voidnoneFires on every change of the digits or the country. value is E.164 or "".
defaultCountry?string"SA"ISO code used when there is no value, and the country of a local-format value such as 0591234567.
countries?readonly PhoneCountry[]PHONE_COUNTRIESThe list to offer.
preferred?readonly string[]PHONE_PREFERREDISO codes listed first, in order. The rest are sorted by name in the active language.
disabled?booleanfalseDisables both parts.
invalid?booleanfalseSets aria-invalid and the danger border.
name?stringnoneRenders a hidden input with the E.164 value, for native form posts.
id?stringnoneId of the digits input.
onFocus? / onBlur?FocusEventHandler<HTMLInputElement>noneFocus and blur of the digits input, for form libraries that validate on blur.
placeholder?stringan example numberPlaceholder of the digits input. Default is an example mobile number for the selected country (50 123 4567), or "Phone number".
locale? / dir?string / "ltr" | "rtl"from the providerOverride for strings, country names and popup direction.
aria-label?stringnoneName of the digits input when there is no FieldLabel.
className?stringnoneMerged onto the group.

Data and helpers

ExportDescription
PhoneCountry{ iso: string; dial: string; en: string; ar: string }. dial has no plus.
PHONE_COUNTRIESEvery country and territory with a calling code (about 245), from libphonenumber, with Intl region names in English and Arabic.
PHONE_PREFERRED["SA", "AE", "EG", "KW", "QA", "BH", "OM", "JO"].
phoneCountryName(iso, locale?)A country's name in any locale (Intl region names, with Palestine as "Palestine" / "فلسطين").
parsePhone(value, countries?){ country, national } | null from an E.164 string. Shared codes resolve by number range.
parsePhoneLenient(value, countries?, fallbackIso?)Like parsePhone, but reads 00966… as +966… and keeps local numbers as national digits of fallbackIso. null only without digits.
formatE164(country, national)E.164 string, or "" when there are no digits. Drops a trunk prefix where the country uses one.
formatNational(country, national)The digits grouped as written after the calling code: 50 123 4567.
phoneExample(country)An example mobile number, grouped the same way, for placeholders.
isValidE164(value)True when the value is a complete number that fits its country's numbering plan.
countryFlag(iso)Flag emoji, for plain text only (a title, a notification). In UI use CountryFlag.

Examples

Prefilled from the server

import { PhoneInput } from "@fadymondy/nasaq/web";

export const Saved = () => <PhoneInput defaultValue="+971501234567" aria-label="Phone" />;

Only some countries

import { PHONE_COUNTRIES, PhoneInput } from "@fadymondy/nasaq/web";

const gcc = PHONE_COUNTRIES.filter((c) => ["SA", "AE", "KW", "QA", "BH", "OM"].includes(c.iso));

export const GulfOnly = () => <PhoneInput countries={gcc} preferred={[]} aria-label="Phone" />;

Arabic and validation in a Field

import { Field, FieldError, FieldLabel, isValidE164, NasaqProvider, PhoneInput } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function PhoneAr() {
  const [phone, setPhone] = useState("");
  const bad = phone !== "" && !isValidE164(phone);
  return (
    <NasaqProvider locale="ar" target="scope">
      <Field invalid={bad}>
        <FieldLabel>رقم الجوال</FieldLabel>
        <PhoneInput value={phone} onValueChange={setPhone} invalid={bad} />
        <FieldError match>أدخل رقمًا كاملًا.</FieldError>
      </Field>
    </NasaqProvider>
  );
}

Accessibility

The country trigger is a combobox named "Country: Saudi Arabia +966"; the popup search input is named "Search countries". The digits input is a Field control, so FieldLabel, FieldDescription and FieldError name and describe it. The country list is inside its own Field scope so it does not take the outer label.

KeyAction
Enter / Space / Down on the triggerOpens the list; focus goes to the search input.
Type in the searchFilters by name (English or Arabic), ISO code or dial code (+20, 20).
Up / DownMoves the highlighted country.
EnterSelects the country and moves focus to the digits input.
EscCloses the list.
  • The flag is decorative (aria-hidden); the name and code are always text.
  • Caller must localise aria-label and any FieldLabel. Built-in strings and country names come in English and Arabic.

RTL & i18n

  • The group mirrors: in Arabic the country trigger sits at the right (start) edge.
  • The digits input is always dir="ltr" and start-aligned, and dial codes are isolated with bdi dir="ltr", so +966 never reverses.
  • Country names follow the active language (English or Arabic, from Intl.DisplayNames) and search matches both.
  • Digits are shown Western (0-9). Arabic-Indic and Persian digits typed or pasted by the user are converted.

Styling & tokens

  • The shell is InputGroup: border-input, bg-card, rounded-control, focus ring nq-focus, invalid nq-danger.
  • The popup uses bg-popover, border-border, shadow-floating; items use nq-selected on highlight.
  • Slots: phone-input, phone-input-country, phone-input-content, phone-input-search.
  • Extend with className; never override colours with raw hex.

Do / Don't

  • Do store and send the E.164 value, and format it for display separately.
  • Do check the number with isValidE164 before submit, and again on the server.
  • Don't put your own +966 in the digits; pick the country or paste the full number.
  • Don't rely on the flag alone to tell countries apart.

Lab

https://docs.nasaqui.com/?path=/docs/components-forms-phone-input--docs

On this page