@kaisarsofi/react-native-phone-input 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +467 -0
  3. package/lib/commonjs/CountryPicker.js +669 -0
  4. package/lib/commonjs/CountryPicker.js.map +1 -0
  5. package/lib/commonjs/Flag.js +23 -0
  6. package/lib/commonjs/Flag.js.map +1 -0
  7. package/lib/commonjs/PhoneInput.js +252 -0
  8. package/lib/commonjs/PhoneInput.js.map +1 -0
  9. package/lib/commonjs/countries.js +75 -0
  10. package/lib/commonjs/countries.js.map +1 -0
  11. package/lib/commonjs/data/countries.json +1472 -0
  12. package/lib/commonjs/index.js +71 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/commonjs/safeArea.js +44 -0
  16. package/lib/commonjs/safeArea.js.map +1 -0
  17. package/lib/commonjs/theme.js +85 -0
  18. package/lib/commonjs/theme.js.map +1 -0
  19. package/lib/commonjs/types.js +6 -0
  20. package/lib/commonjs/types.js.map +1 -0
  21. package/lib/commonjs/value.js +68 -0
  22. package/lib/commonjs/value.js.map +1 -0
  23. package/lib/module/CountryPicker.js +665 -0
  24. package/lib/module/CountryPicker.js.map +1 -0
  25. package/lib/module/Flag.js +19 -0
  26. package/lib/module/Flag.js.map +1 -0
  27. package/lib/module/PhoneInput.js +248 -0
  28. package/lib/module/PhoneInput.js.map +1 -0
  29. package/lib/module/countries.js +65 -0
  30. package/lib/module/countries.js.map +1 -0
  31. package/lib/module/data/countries.json +1472 -0
  32. package/lib/module/index.js +8 -0
  33. package/lib/module/index.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/safeArea.js +39 -0
  36. package/lib/module/safeArea.js.map +1 -0
  37. package/lib/module/theme.js +81 -0
  38. package/lib/module/theme.js.map +1 -0
  39. package/lib/module/types.js +4 -0
  40. package/lib/module/types.js.map +1 -0
  41. package/lib/module/value.js +62 -0
  42. package/lib/module/value.js.map +1 -0
  43. package/lib/typescript/commonjs/package.json +1 -0
  44. package/lib/typescript/commonjs/src/CountryPicker.d.ts +37 -0
  45. package/lib/typescript/commonjs/src/CountryPicker.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/src/Flag.d.ts +10 -0
  47. package/lib/typescript/commonjs/src/Flag.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/src/PhoneInput.d.ts +3 -0
  49. package/lib/typescript/commonjs/src/PhoneInput.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/src/countries.d.ts +28 -0
  51. package/lib/typescript/commonjs/src/countries.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/src/index.d.ts +9 -0
  53. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/safeArea.d.ts +11 -0
  55. package/lib/typescript/commonjs/src/safeArea.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/theme.d.ts +53 -0
  57. package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/src/types.d.ts +211 -0
  59. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/src/value.d.ts +22 -0
  61. package/lib/typescript/commonjs/src/value.d.ts.map +1 -0
  62. package/lib/typescript/module/package.json +1 -0
  63. package/lib/typescript/module/src/CountryPicker.d.ts +37 -0
  64. package/lib/typescript/module/src/CountryPicker.d.ts.map +1 -0
  65. package/lib/typescript/module/src/Flag.d.ts +10 -0
  66. package/lib/typescript/module/src/Flag.d.ts.map +1 -0
  67. package/lib/typescript/module/src/PhoneInput.d.ts +3 -0
  68. package/lib/typescript/module/src/PhoneInput.d.ts.map +1 -0
  69. package/lib/typescript/module/src/countries.d.ts +28 -0
  70. package/lib/typescript/module/src/countries.d.ts.map +1 -0
  71. package/lib/typescript/module/src/index.d.ts +9 -0
  72. package/lib/typescript/module/src/index.d.ts.map +1 -0
  73. package/lib/typescript/module/src/safeArea.d.ts +11 -0
  74. package/lib/typescript/module/src/safeArea.d.ts.map +1 -0
  75. package/lib/typescript/module/src/theme.d.ts +53 -0
  76. package/lib/typescript/module/src/theme.d.ts.map +1 -0
  77. package/lib/typescript/module/src/types.d.ts +211 -0
  78. package/lib/typescript/module/src/types.d.ts.map +1 -0
  79. package/lib/typescript/module/src/value.d.ts +22 -0
  80. package/lib/typescript/module/src/value.d.ts.map +1 -0
  81. package/package.json +215 -0
  82. package/src/CountryPicker.tsx +805 -0
  83. package/src/Flag.tsx +17 -0
  84. package/src/PhoneInput.tsx +363 -0
  85. package/src/countries.ts +95 -0
  86. package/src/data/countries.json +1472 -0
  87. package/src/index.tsx +21 -0
  88. package/src/safeArea.ts +50 -0
  89. package/src/theme.ts +112 -0
  90. package/src/types.ts +229 -0
  91. package/src/value.ts +70 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaisar Sofi
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,467 @@
1
+ # react-native-phone-input
2
+
3
+ **A phone number input that gets validation, formatting, and the country picker right — with zero native modules.**
4
+
5
+ A **React Native phone number input, country code picker, and dial code selector** with live `libphonenumber-js` validation, as-you-type formatting, E.164 output, and an iOS Contacts-style alphabet index on the picker.
6
+
7
+ Works in **Expo Go**, **Expo dev builds**, **bare React Native**, and **`react-native-web`**, on both the old (Paper) and new (Fabric) architecture — because it's pure JavaScript with no native code to reconcile.
8
+
9
+ ![CI](https://github.com/kaisarsofi/react-native-phone-input/actions/workflows/ci.yml/badge.svg)
10
+ ![npm version](https://img.shields.io/npm/v/@kaisarsofi/react-native-phone-input.svg?style=flat-square)
11
+ ![npm downloads](https://img.shields.io/npm/dm/@kaisarsofi/react-native-phone-input.svg?style=flat-square)
12
+ ![license](https://img.shields.io/npm/l/@kaisarsofi/react-native-phone-input.svg?style=flat-square)
13
+ ![types](https://img.shields.io/badge/types-included-3178C6.svg?style=flat-square)
14
+ ![expo](https://img.shields.io/badge/Expo-Go%20%26%20dev%20builds-000.svg?style=flat-square&logo=expo)
15
+ ![new arch](https://img.shields.io/badge/New%20Architecture-supported-61DAFB.svg?style=flat-square)
16
+ ![zero native deps](https://img.shields.io/badge/native%20deps-zero-success.svg?style=flat-square)
17
+
18
+ If this saves you from wiring up validation and a country list by hand, a ⭐ on
19
+ [GitHub](https://github.com/kaisarsofi/react-native-phone-input) keeps it maintained.
20
+
21
+ | iOS | Android | Web |
22
+ | --- | --- | --- |
23
+ | ![iOS demo](docs/iOSExample.gif) | ![Android demo](docs/androidExample.gif) | ![Web demo](docs/webExample.gif) |
24
+
25
+ ---
26
+
27
+ ## Features
28
+
29
+ - ✅ Live phone validation & E.164 formatting via [`libphonenumber-js`](https://github.com/catamphetamine/libphonenumber-js) (pure JS, tree-shakeable — the same engine behind Google's libphonenumber)
30
+ - ⌨️ Formats the number as you type (`AsYouType`)
31
+ - 🌍 245 countries & territories — exact parity with `libphonenumber-js`'s supported regions (ISO 3166-1 codes, ITU dial codes), bundled as plain JSON — no network fetch, no extra install
32
+ - 🔤 A–Z alphabet index on the country picker, tap-to-jump or drag-to-scrub, like iOS Contacts
33
+ - 🎛️ Independent picker layout controls — section headers and the index sidebar toggle separately
34
+ - 🎨 Fully stylable — quick color tokens via `theme`, a plain RN `style` object per element via `pickerStyles`, or full takeover via `renderFlag` / `renderCountryItem`
35
+ - 🧩 NativeWind/shadcn-style-kit compatible — standard `style` prop means `cssInterop` can theme it from your app, no special integration needed
36
+ - 📘 Full TypeScript support
37
+ - 🏗️ Paper and Fabric/New Architecture support
38
+ - 🚫 **Zero native modules** — no `pod install`, no Gradle sync, no config plugin, no `expo prebuild`
39
+
40
+ ## Why this React Native phone input
41
+
42
+ Most phone-input libraries for React Native fall into one of two camps: bundle a native module (so you're stuck with `expo prebuild` and platform builds even for a form field), or skip validation entirely and leave you to hand-roll `libphonenumber-js` yourself. This one does neither.
43
+
44
+ - 📦 **Actually zero setup.** `npm install` and you're done — no native linking, no config plugin, works the same in Expo Go as it does in a bare RN app or on the web.
45
+ - ✅ **Validation isn't bolted on.** Every keystroke runs through `libphonenumber-js` — the same phone-number engine Google's `libphonenumber` is built from — so `isValid` and `e164` are correct per-country, not a regex guess.
46
+ - 🔤 **A picker that scales past 240 countries.** The alphabet index isn't a decoration — it's backed by analytically-computed row offsets (not `onLayout` measurement, which doesn't reliably fire through a `Modal`), so tapping a letter lands exactly on that section every time, on iOS, Android, and web alike.
47
+ - 🎨 **Restyle it, don't fight it.** `pickerStyles` covers every element of the country picker individually — header, search box, row, selected row, section header, sidebar letter — so a full dark-mode or brand re-skin doesn't mean forking the component.
48
+
49
+ ## How it compares
50
+
51
+ | Feature | This package | react-native-phone-number-input | react-native-phone-input | react-native-international-phone-number |
52
+ | --- | --- | --- | --- | --- |
53
+ | Expo Go | ✅ | ⚠️ pulls a native-ish flag dep | ❌ requires a dev build | ✅ |
54
+ | Native modules | ❌ zero | ⚠️ via `react-native-country-picker-modal` | ✅ `@react-native-picker/picker` (hard dep) | ❌ zero |
55
+ | Web (`react-native-web`) | ✅ tested | ⚠️ undocumented | ⚠️ undocumented | ⚠️ undocumented |
56
+ | TypeScript | ✅ | ⚠️ types unmaintained since 2021 | ✅ | ✅ |
57
+ | E.164 output | ✅ | ✅ | ✅ | ✅ |
58
+ | A–Z alphabet index | ✅ | ❌ | ❌ | ❌ |
59
+ | Fabric / New Architecture | ✅ (pure JS) | ? unverified | ? unverified | ? unverified |
60
+ | Auto-detect country from pasted number | ✅ | ❌ | ❌ | ❌ (open feature request, unaddressed) |
61
+ | Last published to npm | — | 2021-05-05 | 2026-06-02 | 2026-03-18 |
62
+
63
+ <sub>Competitor data pulled from the npm registry and each project's GitHub issues; see their repos for current status.</sub>
64
+
65
+ ## Usage
66
+
67
+ ```tsx
68
+ import { PhoneInput, type PhoneInputValue } from '@kaisarsofi/react-native-phone-input';
69
+
70
+ export default function App() {
71
+ return (
72
+ <PhoneInput
73
+ defaultCountry="US"
74
+ onChangeText={(value: PhoneInputValue) => {
75
+ console.log(value.e164); // "+14155552671" | null
76
+ console.log(value.isValid); // true | false
77
+ }}
78
+ />
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### Controlled: one string, no country state
84
+
85
+ Hold the international number and hand it straight back. The country is read
86
+ out of the value, so there is nothing else to track — this is all a form field
87
+ needs:
88
+
89
+ ```tsx
90
+ const [phone, setPhone] = useState(''); // "+919876543210"
91
+
92
+ <PhoneInput value={phone} onChangeInternational={setPhone} />
93
+ ```
94
+
95
+ The same shape drops into react-hook-form:
96
+
97
+ ```tsx
98
+ <Controller
99
+ control={control}
100
+ name="phoneNumber"
101
+ render={({ field: { value, onChange, onBlur } }) => (
102
+ <PhoneInput value={value} onChangeInternational={onChange} onBlur={onBlur} />
103
+ )}
104
+ />
105
+ ```
106
+
107
+ ### Controlled: country and number separately
108
+
109
+ When you do want the two apart:
110
+
111
+ ```tsx
112
+ const [country, setCountry] = useState<CountryCode>('GB');
113
+ const [number, setNumber] = useState('');
114
+
115
+ <PhoneInput
116
+ country={country}
117
+ value={number}
118
+ onChangeCountry={(c) => setCountry(c.code)}
119
+ onChangeText={(v) => setNumber(v.nationalNumber)}
120
+ />
121
+ ```
122
+
123
+ ### Ref API
124
+
125
+ ```tsx
126
+ const ref = useRef<PhoneInputRef>(null);
127
+
128
+ ref.current?.focus();
129
+ ref.current?.isValid();
130
+ ref.current?.getValue(); // PhoneInputValue
131
+ ```
132
+
133
+ ### Restrict / exclude countries
134
+
135
+ ```tsx
136
+ <PhoneInput countries={['US', 'CA', 'GB']} />
137
+ <PhoneInput excludedCountries={['RU', 'KP']} />
138
+ ```
139
+
140
+ ## Install
141
+
142
+ ```bash
143
+ npm install @kaisarsofi/react-native-phone-input
144
+ ```
145
+
146
+ ```bash
147
+ yarn add @kaisarsofi/react-native-phone-input
148
+ ```
149
+
150
+ No peer native dependencies, no `expo install`, no `pod install`. `libphonenumber-js` ships as a regular JS dependency.
151
+
152
+ <details>
153
+ <summary><strong>Requirements</strong></summary>
154
+
155
+ | React Native | React | Expo | Architecture |
156
+ | ------------- | ------------------- | ----------------------------------- | ----------------------- |
157
+ | 0.71+ | 18+ (works with 19) | SDK 49+ (Go, dev builds, prebuild) | Paper and Fabric, both |
158
+
159
+ </details>
160
+
161
+ ## Everything else, in one pass
162
+
163
+ ### What the trigger shows
164
+
165
+ ```tsx
166
+ <PhoneInput displayMode="both" /> {/* default: flag + dial code */}
167
+ <PhoneInput displayMode="flag" /> {/* flag only */}
168
+ <PhoneInput displayMode="code" /> {/* dial code only */}
169
+ ```
170
+
171
+ ### Flag rendering
172
+
173
+ Flags render as the real Unicode flag emoji (e.g. 🇺🇸) — no images, no assets, zero setup. This depends on the device having color flag glyphs in its system font, which nearly every iOS and Android device does. If you need a different look, swap in your own with `renderFlag` (see [Custom rows](#custom-rows) below).
174
+
175
+ ### Country picker: alphabet index & grouping
176
+
177
+ `showAlphabetIndex` (the A–Z sidebar) and `groupAlphabetically` (letter headers in the list) are independent — either can be on without the other:
178
+
179
+ ```tsx
180
+ <PhoneInput showAlphabetIndex={true} groupAlphabetically={true} /> {/* default: both */}
181
+ <PhoneInput showAlphabetIndex={true} groupAlphabetically={false} /> {/* index only — flat list, no headers, sidebar still jumps to the right spot */}
182
+ <PhoneInput showAlphabetIndex={false} groupAlphabetically={true} /> {/* headers only — no sidebar */}
183
+ <PhoneInput showAlphabetIndex={false} groupAlphabetically={false} /> {/* plain flat list */}
184
+ ```
185
+
186
+ The index supports both tap-to-jump and drag-to-scrub, and appears automatically whenever there's more than a handful of letters to jump between. It's hidden while searching.
187
+
188
+ ### Dark mode
189
+
190
+ Both palettes ship complete, and the component follows the OS appearance by
191
+ default — the input and the picker sheet included. Nothing to wire up:
192
+
193
+ ```tsx
194
+ <PhoneInput /> // light or dark, whichever the device is in
195
+ ```
196
+
197
+ Force one, or follow your own app's theme toggle:
198
+
199
+ ```tsx
200
+ <PhoneInput colorScheme={isDark ? 'dark' : 'light'} />
201
+ ```
202
+
203
+ To match a design system, override only the tokens you care about — the rest
204
+ still track the appearance. `palette` covers the input's neutral defaults and
205
+ the whole picker sheet; use `theme` when you want to restyle only the field:
206
+
207
+ ```tsx
208
+ <PhoneInput
209
+ palette={{ accent: '#6366F1', border: '#3F3F46' }}
210
+ />
211
+ ```
212
+
213
+ The full token set is `PhoneInputPalette`; `LIGHT_PALETTE` and `DARK_PALETTE`
214
+ are exported if you want to read the defaults or build one from them.
215
+
216
+ ```ts
217
+ interface PhoneInputPalette {
218
+ background: string; // picker page
219
+ inputBackground: string; // the text field's fill
220
+ surface: string; // search box, section headers, close button, selected row
221
+ surfacePressed: string;
222
+ border: string; // separators, input border, country divider
223
+ text: string;
224
+ textMuted: string; // dial codes, section letters, empty state
225
+ placeholder: string;
226
+ accent: string; // focus ring, A–Z index
227
+ accentContrast: string;
228
+ danger: string; // invalid-number border
229
+ disabled: string; // A–Z letter with no countries
230
+ handle: string; // sheet drag handle
231
+ bubble: string; // letter bubble while scrubbing the index
232
+ bubbleText: string;
233
+ }
234
+ ```
235
+
236
+ ### Theming and full style control
237
+
238
+ `theme` styles the **text field** — its fill, borders and text — and wins over
239
+ the palette's defaults for those. The picker sheet is left to `palette`, so a
240
+ field restyled for a colored background doesn't drag the picker with it:
241
+
242
+ ```tsx
243
+ <PhoneInput
244
+ theme={{
245
+ borderColor: '#D1D1D6',
246
+ focusedBorderColor: '#6366F1',
247
+ errorBorderColor: '#EF4444',
248
+ borderRadius: 12,
249
+ }}
250
+ />
251
+ ```
252
+
253
+ For anything `theme` doesn't cover, every part of the component takes a plain RN `style` object, so nothing is locked in:
254
+
255
+ ```tsx
256
+ <PhoneInput
257
+ style={{ marginTop: 12 }} // outer wrapper (same as containerStyle)
258
+ inputStyle={{ fontSize: 18 }} // the TextInput
259
+ dialCodeStyle={{ fontWeight: '800' }} // the "+1" text
260
+ flagStyle={{ fontSize: 24 }} // the trigger's flag
261
+ countryPickerButtonStyle={{ paddingRight: 12 }} // the flag+code trigger button
262
+ pickerStyles={{ // every part of the modal, individually
263
+ container: { backgroundColor: '#111827' },
264
+ header: { borderBottomWidth: 1, borderBottomColor: '#1F2937' },
265
+ title: { color: '#F9FAFB' },
266
+ search: { backgroundColor: '#1F2937', color: '#F9FAFB' },
267
+ row: { backgroundColor: '#111827' },
268
+ rowSelected: { backgroundColor: '#1F2937' },
269
+ name: { color: '#F9FAFB' },
270
+ dialCode: { color: '#9CA3AF' },
271
+ sidebarLetter: { color: '#818CF8' },
272
+ }}
273
+ />
274
+ ```
275
+
276
+ Since `style` is a standard RN prop name (not a bespoke `containerStyle`-only API), tooling that intercepts `style`/`className` — like NativeWind's `cssInterop`, the mechanism shadcn-style RN kits (e.g. `react-native-reusables`) build on — can theme this component from the consuming app without any special integration on this library's side.
277
+
278
+ ### Safe areas
279
+
280
+ The picker draws into a plain `View`, not React Native's `SafeAreaView` — that
281
+ component is deprecated in favor of `react-native-safe-area-context`, which is
282
+ a native module and would cost this library its zero-native-dependency
283
+ guarantee as a hard dependency. So it is an **optional peer** instead:
284
+
285
+ - **If your app already has it** (every Expo app, and anything using React
286
+ Navigation), the insets are picked up automatically. Nothing to configure.
287
+ - **If it isn't installed**, nothing is required and nothing breaks — the
288
+ import is guarded, which Metro registers as an optional dependency rather
289
+ than a missing one.
290
+
291
+ It is read through `SafeAreaInsetsContext`, not `useSafeAreaInsets()`, since
292
+ that hook throws when no `SafeAreaProvider` is mounted above it — apps carrying
293
+ the package transitively without a provider fall back cleanly instead.
294
+
295
+ The defaults are built to be right without any of that:
296
+
297
+ - On iOS the picker presents as a `pageSheet`, which the system already insets
298
+ at the top.
299
+ - The list uses `contentInsetAdjustmentBehavior="automatic"`, so UIKit clears
300
+ the home indicator at the bottom on its own.
301
+ - On Android `SafeAreaView` was never more than a plain `View` anyway — it is
302
+ iOS-only in React Native.
303
+
304
+ Detected insets are adjusted for the presentation: inside an iOS
305
+ `pageSheet`/`formSheet` the top inset is dropped, because the ambient value
306
+ describes the window and the system has already inset the sheet.
307
+
308
+ To override, pass `pickerSafeAreaInsets`. Each edge you set wins for that edge
309
+ only, so you can opt out of one without losing the others:
310
+
311
+ ```tsx
312
+ <PhoneInput pickerSafeAreaInsets={{ top: 0 }} /> // keep bottom, drop top
313
+ <PhoneInput pickerSafeAreaInsets={{ top: 44, bottom: 34 }} /> // exact values
314
+ ```
315
+
316
+ ### Custom rows
317
+
318
+ Swap out just the flag:
319
+
320
+ ```tsx
321
+ <PhoneInput renderFlag={(country) => <MyFlagIcon iso2={country.code} />} />
322
+ ```
323
+
324
+ Or take over the entire picker row:
325
+
326
+ ```tsx
327
+ <PhoneInput
328
+ renderCountryItem={({ item, isSelected, onPress }) => (
329
+ <Pressable onPress={onPress}>
330
+ <Text style={{ fontWeight: isSelected ? '700' : '400' }}>
331
+ {item.flag} {item.name} (+{item.dialCode})
332
+ </Text>
333
+ </Pressable>
334
+ )}
335
+ />
336
+ ```
337
+
338
+ ## API reference
339
+
340
+ ### `PhoneInputProps`
341
+
342
+ | Prop | Type | Default | Description |
343
+ | --- | --- | --- | --- |
344
+ | `defaultCountry` | `CountryCode` | — | Fixed initial country (uncontrolled). Always wins over locale detection — set this only when you don't want the device locale used |
345
+ | `fallbackCountry` | `CountryCode` | `'US'` | Used only when `defaultCountry` is unset and the device's locale region can't be resolved. Precedence: `defaultCountry` > device locale > `fallbackCountry` |
346
+ | `country` | `CountryCode` | — | Controlled selected country |
347
+ | `value` | `string` | — | Controlled value: either the national number (digits only) or a full international number starting with `+`, in which case the country is derived from it |
348
+ | `onChangeText` | `(value: PhoneInputValue) => void` | — | Fires on every keystroke / country change |
349
+ | `onChangeInternational` | `(value: string) => void` | — | Same trigger as `onChangeText`, but receives `value.international` alone — pairs with the international form of `value` to wire up a form field with no country state of your own |
350
+ | `onChangeCountry` | `(country: Country) => void` | — | Fires only when the country changes |
351
+ | `countries` | `CountryCode[]` | all | Allow-list for the picker |
352
+ | `excludedCountries` | `CountryCode[]` | — | Deny-list for the picker |
353
+ | `disableValidation` | `boolean` | `false` | Turn off the built-in error border |
354
+ | `autoFormat` | `boolean` | `true` | Format the number as you type |
355
+ | `showCountryPicker` | `boolean` | `true` | Show/hide the country control |
356
+ | `displayMode` | `'flag' \| 'code' \| 'both'` | `'both'` | What the trigger shows |
357
+ | `disableSearch` | `boolean` | `false` | Hide the search box in the picker |
358
+ | `groupAlphabetically` | `boolean` | `true` | Group the picker into A–Z sections with letter headers |
359
+ | `showAlphabetIndex` | `boolean` | `true` | Show the A–Z index sidebar (independent of `groupAlphabetically`) |
360
+ | `autoDetectCountry` | `boolean` | `true` | Typing/pasting a number starting with `+` auto-switches the selected country to match its calling code (e.g. pasting `+911234567890` selects India) |
361
+ | `renderFlag` | `(country) => ReactNode` | — | Custom flag renderer |
362
+ | `renderCountryItem` | `(info) => ReactNode` | — | Custom picker row renderer |
363
+ | `theme` | `PhoneInputTheme` | — | Color/radius/font tokens for the **text field** (fill, borders, text, `dividerColor`) |
364
+ | `colorScheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Which palette to draw with. The default follows the OS, so dark mode needs no setup |
365
+ | `palette` | `Partial<PhoneInputPalette>` | — | Override individual palette tokens (separators, surfaces, muted text, the A–Z index…) on top of the resolved light/dark palette — one knob that recolors the input and the picker together |
366
+ | `style` / `containerStyle` | `StyleProp<ViewStyle>` | — | Outer wrapper (`style` is the plain RN name, for style-interop tooling) |
367
+ | `inputStyle` / `dialCodeStyle` / `flagStyle` / `countryPickerButtonStyle` | `StyleProp` | — | Granular style overrides for the trigger row |
368
+ | `pickerStyles` | `CountryPickerStyles` | — | Per-element style overrides for the picker modal — see [Theming and full style control](#theming-and-full-style-control) |
369
+ | `searchPlaceholder` | `string` | `'Search country or code'` | Picker search box placeholder |
370
+ | `pickerSafeAreaInsets` | `{ top?, bottom?, left?, right? }` | — | Safe-area padding for the picker sheet — see [Safe areas](#safe-areas) |
371
+ | `pickerPresentationStyle` | `'pageSheet' \| 'fullScreen' \| 'formSheet'` | `'pageSheet'` (iOS) | Modal presentation style |
372
+
373
+ Any other prop (`placeholder`, `autoFocus`, `onFocus`, `onBlur`, …) is forwarded to the underlying `TextInput`.
374
+
375
+ ### `PhoneInputValue`
376
+
377
+ ```ts
378
+ interface PhoneInputValue {
379
+ nationalNumber: string; // "4155552671"
380
+ country: CountryCode; // "US"
381
+ dialCode: string; // "1"
382
+ e164: string | null; // "+14155552671" or null if invalid
383
+ international: string; // "+1415555" while typing, === e164 once valid, "" when empty
384
+ isValid: boolean;
385
+ }
386
+ ```
387
+
388
+ `international` is the one to store on every change. Unlike `` `+${dialCode}${nationalNumber}` `` it is built through libphonenumber-js, so it stays correct for the 23 NANP territories whose `dialCode` carries a distinguishing area code (Jamaica's is `1876`, but its numbers are `+1876…`, not `+18761876…`).
389
+
390
+ ### `PhoneInputRef`
391
+
392
+ ```ts
393
+ interface PhoneInputRef {
394
+ focus: () => void;
395
+ blur: () => void;
396
+ isValid: () => boolean;
397
+ getValue: () => PhoneInputValue;
398
+ }
399
+ ```
400
+
401
+ ### `CountryPickerStyles`
402
+
403
+ Every named slot is optional and merges on top of the built-in style:
404
+
405
+ ```ts
406
+ interface CountryPickerStyles {
407
+ container?: StyleProp<ViewStyle>;
408
+ headerSection?: StyleProp<ViewStyle>; // raised title + search block above the list
409
+ header?: StyleProp<ViewStyle>;
410
+ title?: StyleProp<TextStyle>;
411
+ closeButton?: StyleProp<ViewStyle>;
412
+ closeButtonText?: StyleProp<TextStyle>;
413
+ searchContainer?: StyleProp<ViewStyle>; // the search field's box
414
+ search?: StyleProp<TextStyle>; // the search field's text input
415
+ row?: StyleProp<ViewStyle>;
416
+ rowSelected?: StyleProp<ViewStyle>;
417
+ flag?: StyleProp<TextStyle>;
418
+ name?: StyleProp<TextStyle>;
419
+ dialCode?: StyleProp<TextStyle>;
420
+ sectionHeader?: StyleProp<ViewStyle>;
421
+ sectionHeaderText?: StyleProp<TextStyle>;
422
+ sidebarLetter?: StyleProp<TextStyle>;
423
+ sidebarLetterActive?: StyleProp<TextStyle>;
424
+ }
425
+ ```
426
+
427
+ ### Also exported
428
+
429
+ - `LIGHT_PALETTE` / `DARK_PALETTE: PhoneInputPalette` — the built-in palettes
430
+ - `usePhoneInputPalette(colorScheme?, overrides?, theme?)` — the hook the components use to resolve a palette
431
+ - `COUNTRIES: Country[]` — the full country dataset (loaded from `src/data/countries.json`)
432
+ - `getCountryByCode(iso2: string): Country | undefined`
433
+ - `groupCountriesByLetter(list: Country[]): CountrySection[]` — the A–Z grouping helper the picker uses internally
434
+ - `Flag` — the flag component standalone (`<Flag country={c} />`)
435
+ - `CountryPicker` — the picker component standalone, if you want to build your own trigger
436
+
437
+ ## Compatibility
438
+
439
+ No native code means no architecture to reconcile: this works identically under the old (Paper) and new (Fabric) architecture, with Expo Go, a custom dev client, a bare RN app, or `react-native-web` — whatever your app already uses. There's no config plugin to add and no `expo prebuild` step required for this library itself.
440
+
441
+ ## Example app
442
+
443
+ ```bash
444
+ git clone https://github.com/kaisarsofi/react-native-phone-input.git
445
+ cd react-native-phone-input && yarn && yarn example web
446
+ ```
447
+
448
+ Toggles every prop (`displayMode`, `showAlphabetIndex`, `groupAlphabetically`) live so you can see the picker behavior change in real time — `yarn example ios` / `yarn example android` work the same way.
449
+
450
+ ## Roadmap
451
+
452
+ - [ ] Optional bundled-flag-image mode for platforms without color emoji font support
453
+ - [ ] Locale-aware country name sorting/translation
454
+ - [ ] RTL layout support for the picker
455
+ - [ ] Recently-used / favorite countries section
456
+
457
+ [Open an issue](https://github.com/kaisarsofi/react-native-phone-input/issues) with a feature request.
458
+
459
+ ## Contributing
460
+
461
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
462
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
463
+ - [Code of conduct](CODE_OF_CONDUCT.md)
464
+
465
+ ## License
466
+
467
+ MIT © [kaisarsofi](https://github.com/kaisarsofi)