npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-phone-country-input

v1.1.1

Published

Phone number input for React Native & Expo — country picker, dial code selector, per-country masking, custom keyboard (no system keyboard flicker). Supports 270+ countries, tablets, iPad.

Readme

react-native-phone-country-input

shows changing the phone number country code

An international phone number input for React Native with country selection, per-country number masking, and a custom keyboard that avoids the native soft keyboard. Ideal for quick changing country codes, entering phone numbers or using bigger devices like tablets that do not have native support for phone number keyboards.

npm version npm downloads GitHub stars MIT License

Peer dependencies

Install these if they are not already in your project:

npm install \
  react-native-reanimated \
  react-native-gesture-handler \
  react-native-teleport \
  expo-localization \
  @react-native-async-storage/async-storage \
  @react-native-clipboard/clipboard

Follow each library's setup guide for any native configuration required.

Installation

npm install react-native-phone-country-input

Features

  • 270+ countries with flags, calling codes, and phone masks
  • Custom keyboard — no system keyboard flicker or layout shift. Great for iPad or Android tablets.
  • Automatic country detection from the device locale on mount
  • Per-country number formatting, in the international style each country uses
  • Country selector modal with search and recently-used countries
  • Copy / paste support with built-in feedback modals (customizable)
  • Cursor position tracking inside masked input
  • Filter countries via allow-list (whitelist) or deny-list (blacklist)
  • Fully customizable: swap in your own TextInput, Pressable, Modal, or feedback modals
  • Dark mode support — pass colorScheme or fine-tune individual color slots
  • Full TypeScript support

Tested on:

  • Xiaomi Redmi Pad 2 9.7 (Android Tablet)
  • Galaxy A06 5G (Android Phone)
  • iPhone 16 Plus
  • iPad Pro 12.9"

Country code modal (with search and history)

Using the Country code modal

On Paste

What happens on paste

Custom Keyboard

Showing Keyboard

Keyboard on Android Tablet

Keyboard on iPad

Setup

Wrap your app (or the screen that uses this component) in PortalProvider and render a PortalHost. The custom keyboard and feedback modals render through a portal to escape z-index stacking.

import { PortalProvider, PortalHost } from 'react-native-teleport';
import { StyleSheet } from 'react-native';

export default function App() {
  return (
    <PortalProvider>
      {/* your app content */}
      <PortalHost style={StyleSheet.absoluteFillObject} name="ipad-keyboard" />
    </PortalProvider>
  );
}

Quick start

PhoneCountryInput is the ready-to-use component. Drop it in and handle the result via onOutcomeChange.

See it in action: App.tsx in this repo is a working demo that uses PhoneCountryInput with custom-styled inputs, a country deny-list, and outcome display — a good starting point to copy from.

import { PhoneCountryInput, PhoneFieldOutcome } from 'react-native-phone-country-input';

export default function MyScreen() {
  const handleChange = (outcome?: PhoneFieldOutcome) => {
    if (outcome?.isValid) {
      console.log('Phone:', outcome.phoneNumber); // digits only, no formatting
      console.log('Country:', outcome.countryDetails?.name);
    }
  };

  return <PhoneCountryInput onOutcomeChange={handleChange} />;
}

PhoneCountryInput props

Extends all TextInput props plus:

| Prop | Type | Required | Description | | --------------------------- | -------------------------------------------- | -------- | -------------------------------------------------------------------- | | onOutcomeChange | (outcome?: PhoneFieldOutcome) => void | Yes | Called whenever the phone number or country changes | | allowedCountryCodes | CountryId[] | No | Show only these countries in the selector | | disallowedCountryCodes | CountryId[] | No | Hide these countries from the selector | | defaultCountry | CountryId | No | Country to start on when the device's region is not an available one | | underlineInput | React.ComponentType | No | Replace the default TextInput with your own styled component | | underlineButton | React.ComponentType | No | Replace the country selector button with your own styled Pressable | | underlineModal | React.ComponentType | No | Replace the country selector modal entirely | | underlinePasteErrorModal | React.ComponentType<PasteErrorModalProps> | No | Replace the built-in "cannot paste" modal | | underlineCopySuccessModal | React.ComponentType<CopySuccessModalProps> | No | Replace the built-in "copied" confirmation modal | | underlineChevron | React.ComponentType<ChevronProps> | No | Replace the selector button's chevron with your own icon | | keyboard | 'custom' \| 'native' | No | Which keypad enters digits. Defaults to 'custom' | | colorScheme | 'light' \| 'dark' | No | Picks the built-in light or dark palette. Defaults to light | | countrySelectorColors | Partial<CountrySelectorColors> | No | Per-slot color overrides for the country picker | | keyboardColors | Partial<KeyboardColors> | No | Per-slot color overrides for the custom keypad | | style | ViewStyle | No | Style applied to the outer row container |

Which country it starts on

The device's region, from expo-localization. If that region is not one of the countries you allow, defaultCountry is used instead:

<PhoneCountryInput
  allowedCountryCodes={[CountryId.UNITED_STATES, CountryId.CANADA]}
  defaultCountry={CountryId.UNITED_STATES}
  onOutcomeChange={setOutcome}
/>

Worth setting. Without it the last resort is the first country in the internal list, which is ordered by dial-code length for number parsing and so is close to arbitrary.

Choosing the keypad

The custom keypad is built for tablets. Phones are usually better off with the system one:

import DeviceInfo from 'react-native-device-info';

<PhoneCountryInput
  keyboard={DeviceInfo.isTablet() ? 'custom' : 'native'}
  onOutcomeChange={setOutcome}
/>;

Replacing the chevron

Swap the selector button's chevron for your own icon. You get the open state and the current theme color:

import { ChevronDownIcon, ChevronUpIcon } from 'react-native-heroicons/outline';
import type { ChevronProps } from 'react-native-phone-country-input';

function Chevron({ isOpen, color, size }: ChevronProps) {
  const Icon = isOpen ? ChevronUpIcon : ChevronDownIcon;
  return <Icon size={size} color={color} />;
}

<PhoneCountryInput underlineChevron={Chevron} onOutcomeChange={setOutcome} />;

Custom styling example

import { TextInput, Pressable } from 'react-native';
import { PhoneCountryInput } from 'react-native-phone-country-input';

function StyledInput(props: React.ComponentProps<typeof TextInput>) {
  return (
    <TextInput
      {...props}
      style={[
        { flex: 1, borderWidth: 1, borderColor: '#ccc', padding: 10, borderRadius: 8 },
        props.style,
      ]}
    />
  );
}

function StyledButton(props: React.ComponentProps<typeof Pressable>) {
  return (
    <Pressable
      {...props}
      style={{ padding: 10, borderWidth: 1, borderColor: '#ccc', borderRadius: 8 }}>
      {props.children}
    </Pressable>
  );
}

export default function MyScreen() {
  return (
    <PhoneCountryInput
      underlineInput={StyledInput}
      underlineButton={StyledButton}
      onOutcomeChange={(outcome) => console.log(outcome)}
    />
  );
}

Country filtering example

import { PhoneCountryInput, CountryId } from 'react-native-phone-country-input';

const ALLOWED = [CountryId.UNITED_STATES, CountryId.CANADA, CountryId.MEXICO];

<PhoneCountryInput
  allowedCountryCodes={ALLOWED}
  onOutcomeChange={(outcome) => console.log(outcome)}
/>;

Copy and paste feedback modals

When the user taps Copy, the keyboard shows a "Copied" confirmation modal. When the user taps Paste and the clipboard does not contain a recognisable phone number, a "Cannot Paste" error modal appears. Both modals have built-in defaults and can be replaced with your own UI.

Default behaviour

No configuration needed — the defaults work out of the box:

Copy success

a modal with the title "Copied" and the message "Phone number copied to clipboard."

Paste error

a modal with the title "Cannot Paste" and the message "Clipboard does not contain a valid phone number."

Paste error

Replacing the paste-error modal

Pass a component that accepts PasteErrorModalProps to underlinePasteErrorModal:

import { Modal, View, Text, Pressable } from 'react-native';
import { PhoneCountryInput, PasteErrorModalProps } from 'react-native-phone-country-input';

function MyPasteErrorModal({ isPasteErrorVisible, dismissPasteError }: PasteErrorModalProps) {
  return (
    <Modal
      visible={isPasteErrorVisible}
      transparent
      animationType="fade"
      onRequestClose={dismissPasteError}>
      <Pressable
        style={{
          flex: 1,
          backgroundColor: 'rgba(0,0,0,0.5)',
          justifyContent: 'center',
          alignItems: 'center',
        }}
        onPress={dismissPasteError}>
        <View style={{ backgroundColor: '#fff', borderRadius: 12, padding: 24, width: 280 }}>
          <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 8 }}>Invalid number</Text>
          <Text style={{ color: '#666' }}>
            Paste a phone number with a country code (e.g. +1 555 123 4567).
          </Text>
          <Pressable onPress={dismissPasteError} style={{ marginTop: 16, alignSelf: 'flex-end' }}>
            <Text style={{ color: '#007AFF', fontWeight: '600' }}>Got it</Text>
          </Pressable>
        </View>
      </Pressable>
    </Modal>
  );
}

<PhoneCountryInput
  underlinePasteErrorModal={MyPasteErrorModal}
  onOutcomeChange={(outcome) => console.log(outcome)}
/>;

Replacing the copy-success modal

Pass a component that accepts CopySuccessModalProps to underlineCopySuccessModal:

import { Modal, View, Text, Pressable } from 'react-native';
import { PhoneCountryInput, CopySuccessModalProps } from 'react-native-phone-country-input';

function MyCopySuccessModal({ isCopySuccessVisible, dismissCopySuccess }: CopySuccessModalProps) {
  return (
    <Modal
      visible={isCopySuccessVisible}
      transparent
      animationType="fade"
      onRequestClose={dismissCopySuccess}>
      <Pressable
        style={{ flex: 1, justifyContent: 'flex-end', padding: 16 }}
        onPress={dismissCopySuccess}>
        <View
          style={{
            backgroundColor: '#22c55e',
            borderRadius: 12,
            padding: 16,
            flexDirection: 'row',
            alignItems: 'center',
            gap: 12,
          }}>
          <Text style={{ color: '#fff', fontSize: 15, fontWeight: '600' }}>
            ✓ Copied to clipboard
          </Text>
        </View>
      </Pressable>
    </Modal>
  );
}

<PhoneCountryInput
  underlineCopySuccessModal={MyCopySuccessModal}
  onOutcomeChange={(outcome) => console.log(outcome)}
/>;

Building your own UI with individual pieces

Import the sub-components and usePhoneFieldState to compose a completely custom layout.

import {
  usePhoneFieldState,
  PhoneNumberField,
  CountrySelector,
  Keyboard,
  PhoneFieldOutcome,
} from 'react-native-phone-country-input';
import { View } from 'react-native';

export default function CustomPhoneInput({
  onResult,
}: {
  onResult: (o: PhoneFieldOutcome) => void;
}) {
  const {
    // State
    outcome,
    phoneNumber,
    filteredCountryCodes,
    cursorPosition,
    isKeyboardOpen,
    isCountrySelectorOpen,
    // Handlers
    onChangeText,
    onChangeFlag,
    onKeyPress,
    onTextSelection,
    onCopy,
    onPaste,
    openKeyboard,
    closeKeyboard,
    openCountrySelector,
    closeCountrySelector,
  } = usePhoneFieldState({
    // optional: allowedCountryCodes, disallowedCountryCodes
  });

  return (
    <>
      <View style={{ flexDirection: 'row', gap: 8 }}>
        <CountrySelector
          value={outcome?.countryDetails ?? null}
          filtedredCountryCodes={filteredCountryCodes}
          onSelectCountry={onChangeFlag}
          isOpen={isCountrySelectorOpen}
          onOpenChange={(open) => (open ? openCountrySelector() : closeCountrySelector())}
        />
        <PhoneNumberField
          style={{ flex: 1 }}
          value={phoneNumber}
          onChangeText={onChangeText}
          onOpen={openKeyboard}
          onTextSelection={onTextSelection}
          selection={cursorPosition}
        />
      </View>
      <Keyboard
        value={phoneNumber}
        isOpen={isKeyboardOpen}
        onClose={closeKeyboard}
        onKeyPress={onKeyPress}
        onCopy={onCopy}
        onPaste={onPaste}
      />
    </>
  );
}

API reference

usePhoneFieldState(params)

The state management hook that powers PhoneCountryInput. Returns all state and handlers needed to wire up the sub-components independently.

Params

| Field | Type | Description | | ------------------------ | ------------- | ------------------------- | | allowedCountryCodes | CountryId[] | Show only these countries | | disallowedCountryCodes | CountryId[] | Hide these countries |

Returns

| Field | Type | Description | | ----------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | outcome | PhoneFieldOutcome \| undefined | Current validation result | | phoneNumber | string \| undefined | Raw unmasked digits (no +) | | country | CountryCode \| undefined | Currently selected country | | filteredCountryCodes | CountryCode[] | Country list after filtering | | cursorPosition | { start: number; end: number } | Cursor position in the masked display string | | isKeyboardOpen | boolean | Whether the custom keyboard is visible | | isCountrySelectorOpen | boolean | Whether the country selector modal is open | | onChangeText | (value: string) => void | Pass to PhoneNumberField.onChangeText | | onChangeFlag | (country: CountryCode) => void | Pass to CountrySelector.onSelectCountry | | onKeyPress | (key: KEYPAD_KEY) => void | Pass to Keyboard.onKeyPress | | onTextSelection | (e: NativeSyntheticEvent<...>) => void | Pass to PhoneNumberField.onTextSelection | | onCopy | () => void | Copies the current number to the clipboard | | onPaste | () => Promise<boolean> | Pastes from clipboard — resolves true on success, false if the clipboard content is not a recognisable phone number | | onClearText | () => void | Resets the field to the country code only | | openKeyboard | () => void | — | | closeKeyboard | () => void | — | | openCountrySelector | () => void | — | | closeCountrySelector | () => void | — |


PhoneNumberField

The masked text input. Renders a TextInput (or your custom component) prefixed with +. Always passes showSoftInputOnFocus={false} — use Keyboard for input.

| Prop | Type | Description | | ----------------- | -------------------------------- | --------------------------------------------------------- | | value | string | Unmasked phone number (digits only, no +) | | onChangeText | (value: string) => void | Text change handler | | onOpen | () => void | Called when the field is pressed — open the keyboard here | | onTextSelection | (e) => void | Selection change handler for cursor tracking | | selection | { start: number; end: number } | Controlled cursor position | | underlineInput | React.ComponentType | Custom TextInput replacement |


CountrySelector

The country flag button + selector modal. Supports both controlled (isOpen) and uncontrolled usage.

| Prop | Type | Description | | ----------------------- | -------------------------------- | -------------------------------------------------------------------- | | value | CountryCode \| null | Currently selected country | | onSelectCountry | (country: CountryCode) => void | Called when the user picks a country | | filtedredCountryCodes | CountryCode[] | The country list to display | | isOpen | boolean | Controlled open state | | onOpenChange | (open: boolean) => void | Called when open state should change | | underlineButton | React.ComponentType | Custom button replacing the flag+chevron | | underlineModal | React.ComponentType | Custom modal replacing the built-in sheet | | colorScheme | 'light' \| 'dark' | Select the built-in light or dark color preset | | colors | Partial<CountrySelectorColors> | Override individual color slots (merged on top of the active preset) |


Theming the country picker

By default the picker uses a light color palette. Pass colorScheme to switch to the built-in dark preset, or pass colors to override individual slots. CountrySelector and CountrySelectorModal take both; on PhoneCountryInput the overrides are named countrySelectorColors, since it themes the keypad too.

Follow the system scheme

import { useColorScheme } from 'react-native';
import { CountrySelector } from 'react-native-phone-country-input';

const scheme = useColorScheme(); // 'light' | 'dark' | null

<CountrySelector
  colorScheme={scheme ?? 'light'}
  value={country}
  onSelectCountry={setCountry}
  ...
/>

If you manage dark mode yourself (via a theme context, Redux, etc.) just pass the string directly:

const { isDark } = useMyTheme();

<CountrySelector colorScheme={isDark ? 'dark' : 'light'} ... />

Override individual slots

colors is merged on top of whichever preset colorScheme selects, so you can tweak one slot without rebuilding the whole palette:

// Dark base, custom accent
<CountrySelector
  colorScheme="dark"
  colors={{ accent: '#a855f7', itemSelectedBackground: '#3b0764' }}
  ...
/>

Ship your own full palette

import { CountrySelectorColors } from 'react-native-phone-country-input';

const brandColors: CountrySelectorColors = {
  background: '#1a1a2e',
  sheet: '#16213e',
  searchBorder: '#0f3460',
  searchBackground: '#0f3460',
  searchText: '#ffffff',
  searchPlaceholder: '#a8a8b3',
  sectionHeaderText: '#a8a8b3',
  itemSelectedBackground: '#e94560',
  accent: '#e94560',
  subtext: '#a8a8b3',
  text: '#ffffff',
};

<CountrySelector colors={brandColors} ... />

The two built-in presets are exported if you want to extend them:

import { defaultLightColors, defaultDarkColors } from 'react-native-phone-country-input';

Theming the keypad

Same idea as the picker, with keyboardColors on PhoneCountryInput (or colors if you render Keyboard yourself):

<PhoneCountryInput
  colorScheme="dark"
  keyboardColors={{ key: '#2a2a3e', keyText: '#ffffff' }}
  onOutcomeChange={setOutcome}
/>

Both presets are exported:

import {
  defaultLightKeyboardColors,
  defaultDarkKeyboardColors,
} from 'react-native-phone-country-input';

Keyboard

The custom phone keypad. Renders via a PortalHost and animates in/out with react-native-reanimated. Manages copy/paste feedback modals internally.

| Prop | Type | Description | | --------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------- | | isOpen | boolean | Whether the keyboard is visible | | onClose | () => void | Called when the Done button is pressed | | onKeyPress | (key: KEYPAD_KEY) => void | Called for every key tap | | value | string | Current phone number string (shown in the toolbar) | | onCopy | () => void | Called when the Copy button is pressed | | onPaste | () => Promise<boolean> | Called when the Paste button is pressed — return false to trigger the paste-error modal | | underlinePasteErrorModal | React.ComponentType<PasteErrorModalProps> | Replace the built-in "cannot paste" modal | | underlineCopySuccessModal | React.ComponentType<CopySuccessModalProps> | Replace the built-in "copied" confirmation modal | | colorScheme | 'light' \| 'dark' | Picks the built-in light or dark palette | | colors | Partial<KeyboardColors> | Per-slot color overrides |


Types

PhoneFieldOutcome

interface PhoneFieldOutcome {
  phoneNumber: string; // digits only, no formatting, no leading '+'
  isValid: boolean; // true when digit count matches the country's mask
  correctLength: number; // total character length when fully formatted (e.g. "+1 555-123-4567".length)
  countryDetails: CountryCode | null;
}

PasteErrorModalProps

Received by a custom paste-error modal component.

interface PasteErrorModalProps extends React.ComponentProps<typeof Modal> {
  isPasteErrorVisible: boolean; // whether the modal should be visible
  dismissPasteError: () => void; // call this to close the modal
}

CopySuccessModalProps

Received by a custom copy-success modal component.

interface CopySuccessModalProps extends React.ComponentProps<typeof Modal> {
  isCopySuccessVisible: boolean; // whether the modal should be visible
  dismissCopySuccess: () => void; // call this to close the modal
}

CountryCode

interface CountryCode {
  id: CountryId; // e.g. CountryId.UNITED_STATES
  code: string; // e.g. '+1'
  flag: string; // emoji flag, e.g. '🇺🇸'
  name: string; // e.g. 'United States'
  mask: string; // formatting template, e.g. ' ###-###-####' -> '+1 651-555-0123'
}

Masks start with a space so the dial code stays separate from the rest of the number. Groups are space-separated, the international convention — except +1 countries, which keep the hyphens North America expects.

CountrySelectorColors

Color slots accepted by the colors prop on CountrySelector and CountrySelectorModal. All fields are optional when passed as Partial<CountrySelectorColors>.

interface CountrySelectorColors {
  background: string; // container, section headers, and dividers
  sheet: string; // search area and row background
  searchBorder: string; // search box border
  searchBackground: string; // search box fill
  searchText: string; // text typed into the search box
  searchPlaceholder: string; // search box placeholder
  sectionHeaderText: string; // section letter / label text
  itemSelectedBackground: string; // selected row highlight
  accent: string; // checkmark icon and selected-row text weight
  subtext: string; // country code + mask hint text, search icon, chevron
  text: string; // country name and dial code
}

Two ready-made presets are exported:

import { defaultLightColors, defaultDarkColors } from 'react-native-phone-country-input';

KeyboardColors

Color slots accepted by keyboardColors on PhoneCountryInput, or colors on Keyboard.

interface KeyboardColors {
  sheet: string; // the keypad panel
  key: string; // digit key face
  keyPressed: string; // digit key while held
  keyTransparent: string; // globe + backspace keys, which have no face
  keyTransparentPressed: string; // those keys while held
  keyText: string; // digits, `clear`, and key icons
  keySubtext: string; // the ABC / DEF letter hints
  toolbarText: string; // copy / paste icons and `Done`
  toolbarTextDisabled: string; // copy icon with nothing to copy
  dialog: string; // copy / paste dialog background
  dialogTitle: string;
  dialogMessage: string;
}
import {
  defaultLightKeyboardColors,
  defaultDarkKeyboardColors,
} from 'react-native-phone-country-input';

ChevronProps

Received by a custom chevron passed to underlineChevron.

interface ChevronProps {
  isOpen: boolean; // whether the picker is open
  color: string; // resolved from the current palette
  size: number;
}

CountryId

An enum with an entry for every supported country, e.g. CountryId.UNITED_STATES, CountryId.UNITED_KINGDOM, CountryId.CANADA. Import it to build allow/deny lists.

import { CountryId } from 'react-native-phone-country-input';

Contributing

Pull requests are welcome. To work on the library locally, clone the repo and run the Expo demo app from the project root:

npm install
npm start        # starts the Expo dev server
npm run build    # compiles the library to lib/
npm test         # runs the unit test suite

Inspired by