react-native-telephone
v2.1.0
Published
Zero-dependency React Native international phone input with per-country masking and validation (runs on web via react-native-web)
Downloads
915
Maintainers
Readme
react-native-telephone
An international phone input for React Native — and for the web, via react-native-web. You type a number and get a clean E.164 string back, with per-country masking, a searchable country picker, and live validation for 250 countries.
Features
- Minimal dependencies. No
react-native-svg, no masking library, no flag assets. Onlyreact,react-native, andcountry-data-tsfor the phone dataset. - E.164 in, E.164 out. The value you hold is always ready to store or send.
- Per-country masking that formats the national number as you type.
- Searchable country picker with a flag button and 250 countries.
- Live validation against each country's national-number pattern.
- Smart paste that recognizes a full international number and switches country for you.
- Headless helpers for parsing, formatting, and validating without any UI.
Contents
- Installation
- Quick start
- How it works
- Controlled value
- Props
- Styling
- Validation
- Headless use
- Development
- License
Installation
npm install react-native-telephoneQuick start
import { useState } from "react";
import { PhoneInput } from "react-native-telephone/phone-input";
function Example() {
const [phone, setPhone] = useState("");
return (
<PhoneInput
label="Phone number"
value={phone}
onChangeText={setPhone}
defaultCountry="US"
/>
);
}value is always the E.164 string (+14155550123), so it's ready to store or
send as-is. The masked, human-readable version ((415) 555-0123) lives inside
the component — you never have to parse it back.
How it works
The field is split into two inputs behind one border: an editable calling
code (+1, +33, …) and the national number. Type digits into the
national side and they're formatted live against that country's mask. The flag
button on the left opens a searchable picker.
Editing the calling code — or picking a country — reflows the number under the new mask and keeps the digits you already typed.
A few behaviors worth knowing:
- Backspace at the start of an empty national field steps back into the calling code, so you can delete your way back to switch countries.
- Shared calling codes (every
+1country,+44for the UK and its dependencies) stay on your current selection instead of snapping to the first match. So a US number doesn't flip to Antigua the moment you type+1. - Smart paste. Pasting a full international number —
+1 (204) 234-2222— into either field recognizes the country code (and, for shared codes, the area code), switches country if needed, and formats the national number correctly.+1 204…lands on Canada; a stray trunk prefix is stripped where the mask expects it (+33 0612345678→6 12 34 56 78). A plain national number that fits the current country is never misread as a foreign calling code. - Typed separators are honored. Typing the mask's own
(,),-, or space shows it immediately instead of swallowing it until the next digit arrives. Separators typed in the wrong place are dropped so digits stay aligned. - Validation uses each country's national-number pattern. By default the error appears once the number fills the mask, or on blur — see Validation.
Controlled value
PhoneInput is fully controlled. Feed value back through onChangeText (as in
the quick start) and it stays in sync. You can also hand it an existing E.164
string and it hydrates the country, calling code, and mask from it:
<PhoneInput value="+33612345678" onChangeText={setPhone} />
// → 🇫🇷 +33 6 12 34 56 78Props
Two props are required: value and onChangeText. Everything else is optional.
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| value | string | — | Controlled E.164 value. Empty string when blank. Required. |
| onChangeText | (value: string) => void | — | Fires with the next E.164 value on every edit. Required. |
| onCountryChange | (country: CountryCode) => void | — | Fires whenever the country changes. |
| onValidationChange | (isValid: boolean) => void | — | Fires with validity on every edit and on blur. |
| allowedCountries | CountryCode[] | all 250 | Restricts the picker to these countries, in this exact order. Omit for all 250, alphabetized. |
| defaultCountry | CountryCode \| null | locale → first | Initial country when value has none. |
| locale | string | device locale | Localizes country names and picks a default. |
| label | string | — | Label above the field. |
| error | string | — | External error; overrides the built-in one. |
| hint | string | — | Helper text below the field (hidden while an error shows). |
| invalidError | string | "Invalid phone number" | Message for a failed validation. |
| validationMode | "onType" \| "onBlur" \| "never" | "onType" | When the built-in error appears. |
| placeholder | string | country example | National-field placeholder. |
| editable | boolean | true | |
| autoFocus | boolean | false | |
| size | "sm" \| "md" \| "lg" | "md" | |
| testID | string | — | Sub-elements get -calling-code, -national, -error suffixes. |
Styling and render props are covered in Styling.
Styling
Three levels, from lightest touch to full control.
1. Per-slot overrides via styles — a StyleProp for each piece of the
default UI (field, nationalInput, option, panel, …). Merged on top of
the defaults, so you only set what you want to change:
<PhoneInput
value={phone}
onChangeText={setPhone}
styles={{
field: { borderColor: "#111", borderRadius: 12 },
nationalInput: { fontFamily: "Menlo" },
optionSelected: { backgroundColor: "#fef3c7" },
}}
/>style restyles just the root column; styles.field restyles the bordered
input row. The full slot list is the PhoneInputStyles type, and the default
values live in the exported defaultStyles / COLORS / SIZES if you want to
extend rather than replace them.
2. Render props when a slot isn't enough and you need different markup:
renderFlag— draw the flag however you like (SVG, PNG, a library). Gets{ code, emoji, size }; return any node. This is the escape hatch for the Android emoji caveat below.renderContainer— replace the label / field / error / hint shell around the input row.renderCountryPicker— replace the whole picker modal. Gets the filtered options, selection, and anonSelectcallback, so you can drop in a bottom sheet or your own list.
import { Image } from "react-native";
<PhoneInput
value={phone}
onChangeText={setPhone}
renderFlag={({ code }) => (
<Image source={{ uri: `https://flagcdn.com/32x24/${code.toLowerCase()}.png` }} style={{ width: 26, height: 18 }} />
)}
/>A note on flags
Flags render as emoji by default. A country's flag emoji is literally its two
ISO letters as Unicode regional-indicator symbols, so there's no image asset or
flag library involved. That's what keeps the package nearly dependency-free — the only runtime dependency is country-data-ts for the phone dataset.
The catch: iOS, macOS, and browsers show them; stock Android does not (it
falls back to the two letters, e.g. boxed "US"). If you ship to Android and want
real flags, pass renderFlag with an image or SVG source — the component never
hard-codes the emoji, it just calls your renderer.
Validation
Every country carries a national-number regex. onValidationChange fires with
the true validity on each edit and on blur, regardless of what's shown on
screen. What validationMode controls is only when the built-in error message
appears:
"onType"(default) — as soon as the number fills the mask's required digits, or on blur."onBlur"— only after the field loses focus."never"— never shows the built-in error; drive your own fromonValidationChangeor theerrorprop.
Headless use
Need to parse, format, or validate without the UI? The subpaths below are pure — no React, no React Native — so they're safe on a server or in a plain Node script.
| Subpath | Contents |
| --- | --- |
| react-native-telephone/phone | Phone utilities + CountryCode, CountryPhoneConfig, data constants |
| react-native-telephone/options | buildCountryOptions, getRegionLabel, normalizeForSearch |
| react-native-telephone/emoji | countryCodeToEmoji (no React Native import) |
| react-native-telephone/flags | defaultRenderFlag, countryCodeToEmoji |
| react-native-telephone/styles | defaultStyles, COLORS, SIZES |
| react-native-telephone/types | PhoneInputStyles, RenderCountryPickerProps, and related types |
| react-native-telephone/codes | COUNTRY_CODES, CountryCode, isCountryCode |
import { toE164, validateExtractedPhone, getCountryPhoneConfig } from "react-native-telephone/phone";
const fr = getCountryPhoneConfig("FR")!;
toE164("0612345678", fr); // "+33612345678" (trunk 0 dropped)
validateExtractedPhone("612345678", fr); // trueimport { COUNTRY_CODES, type CountryCode } from "react-native-telephone/codes";
// or directly from the data package:
import { COUNTRY_CODES, type CountryCode } from "country-data-ts/countries";Development
bun install
bun run typecheck # tsc --noEmit
bun run lint # biome
bun run test # vitest
bun run build # tsup → dist/
bun run demo # web demo via react-native-webThe phone dataset comes from country-data-ts.
License
MIT
