Skip to content
Native Animation
Phone Country Input animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add phone-country-input

Phone Country Input

A phone field with a searchable country picker and cursor-aware formatting, with configurable countries, labels, palette, and fonts.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

Related guides

A phone field with a searchable country picker and cursor-aware formatting, with configurable countries, labels, palette, and fonts.

How it works

  1. Keep ISO country identity separate from national digits. Countries that share a dial code, such as US and CA, remain distinct.
  2. Normalize decimal digits without truncating. A consumer formatter receives digit offsets and returns formatted text and text offsets. A formatter that throws or changes digits falls back to unformatted digits.
  3. Map both selection endpoints by their preceding digit counts. Deleting only a separator removes the adjacent digit in the deletion direction. The same input stays mounted through country changes and validation.
  4. Validate through a synchronous consumer adapter. Without one, changes emit e164: null; this component does not implement worldwide phone validation.
  5. Drive the contained country sheet and scrim from one interruptible UI-thread spring. Search is immediate and does not change the phone value until a country is selected.
  6. Coordinate helper text and error border over 160 ms. Host errors persist until the host removes them. The demo corrects a middle digit before opening search, using its own animation timings rather than measured video timing.

Install

Copy the component files listed in piece.json into your project, then install:

npx expo install react-native-reanimated react-native-worklets

Use Reanimated's Expo SDK setup and New Architecture. No phone library, network country service, images, safe area provider, or gesture wrapper is required by the component. The optional catalog demo additionally uses Expo fonts, safe area context, and Skia; these are not component dependencies.

Usage

import { PhoneCountryInput } from './phone-country-input';

const countries = [
  { iso2: 'US', name: 'United States', dialCode: '+1' },
  { iso2: 'CA', name: 'Canada', dialCode: '+1' },
  { iso2: 'GB', name: 'United Kingdom', dialCode: '+44' },
];

export function PhoneForm() {
  return (
    <PhoneCountryInput
      countries={countries}
      defaultValue={{ countryIso2: 'US', nationalNumber: '' }}
      labels={{ title: 'Your phone number', country: 'Country or region' }}
    />
  );
}

Give the component a bounded parent with flex: 1, or override its root size with style. The host owns safe areas. The component owns its form scroll and country list scroll; the sheet stays inside that host instead of mounting a global modal.

Controlled integration with personality knobs:

const [value, setValue] = useState({
  countryIso2: 'US',
  nationalNumber: '',
});
const control = useRef<PhoneCountryInputHandle>(null);

<PhoneCountryInput
  ref={control}
  countries={countries}
  value={value}
  onChange={setValue}
  validateNumber={phoneAdapter.validate}
  parseInternational={phoneAdapter.parse}
  colorScheme="dark"
  palette={{ accent: '#76D6C4', focus: '#76D6C4' }}
  motion={{ durationScale: 0.85, glowOpacity: 0.12 }}
  helperText="Choose your country, then enter your number."
  validText="Number confirmed."
/>;

Import useRef, useState, and PhoneCountryInputHandle for the second example. phoneAdapter belongs to your application; its validator must check numbering rules rather than length alone. It should return { valid, error?, e164? }, with a complete E.164 string only on success.

Formatting example (does not impose a length limit):

import { displaySelection } from './phone-country-input/model';
import type { PhoneCountryInputProps } from './phone-country-input';

const formatNumber: NonNullable<PhoneCountryInputProps['formatNumber']> = (
  digits,
  country,
  { selection },
) => {
  const text = digits.length > 3
    ? `${digits.slice(0, 3)} ${digits.slice(3)}`
    : digits;
  return { text, selection: displaySelection(text, selection) };
};

Formatter input selection refers to offsets in the raw digit string. Output selection, public selection, and change metadata refer to offsets in formatted text. A custom formatter must preserve all digits, including long and incomplete input. onChange fires once per committed edit, country selection, setValue, or reset; selection-only changes call onSelectionChange without a value callback.

International paste is delegated to parseInternational(text, countries). A successful parse supplies both ISO identity and national digits. Without it, or if it throws/returns null, all pasted digits remain under the current country. A shared +1 never chooses between US and CA. Browser paste is detected explicitly; native insertion of multiple characters is classified as paste when the inserted text grows by more than one character.

Props

PropTypeDefaultPurpose
countriesreadonly PhoneCountry[]RequiredISO, name, dial code, optional search terms and disabled state.
value / defaultValuePhoneValueFirst enabled country, empty digitsControlled value / uncontrolled initial and reset value.
onChange(value, meta) => void—Formatted text, E.164 or null, selection, and reason: edit/country/paste/reset.
formatNumber(digits, country, {selection}) => PhoneFormatDigits onlyPreserves digits and maps selection.
validateNumber(digits, country) => PhoneValidationNo validationSynchronous consumer validation. Exceptions fail validation.
parseInternational(text, countries) => PhoneValue | nullPreserve all digitsConsumer policy for pasted international numbers.
selection / onSelectionChangePhoneSelection / callbackInternal selectionControlled selection request/response.
open / onOpenChangeboolean / callbackInternal, closedControlled country sheet.
query / onQueryChangestring / callbackInternal, emptyIndependent local search.
renderFlag(country) => ReactNodeNo flagConsumer artwork; ISO text is always shown.
disabledbooleanfalseBlocks interaction and handle actions except reset.
readOnlybooleanfalsePrevents value changes and country selection.
errorstring—Persistent external error, takes priority over adapter errors.
onBlur() => void—Native number input blur.
onSubmit(value, meta) => void—Continue/keyboard submit after consumer validation.
labelsPartial<PhoneCountryInputLabels>Neutral EnglishTitle, phone, country, search, close, continue, empty, invalid, selectedCountry(country), resultCount(count).
helperText / validTextstringEmptyReserved helper area and confirmed-number message.
colorSchemelight | dark | autoautoPaper, Night, or system.
palettePartial<Palette>Theme tokensbackground, surface, elevated, text, muted, border, accent, onAccent, accentSoft, glow, success, error, scrim, track, focus.
fontFamily / titleFontFamily / monoFontFamilystringSystem fontsBody, title/buttons, and number.
motion{durationScale?, glowOpacity?}1, 0.12Clamped to 0.5–2 and 0–0.3; nonfinite values use defaults.
styleStyleProp<ViewStyle>—Root override, applied last.
testIDstringphone-country-inputRoot; stable suffixes -country, -number, -help, -continue, -search, -close, -scrim, -option-<ISO>.
accessibilityLabelstring—Optional root description.

The handle exports focus(), blur(), openCountries(), closeCountries(), setValue(value), and reset(). Controlled props always win: handle methods request changes through callbacks instead of overriding controlled state. setValue emits reason edit; reset emits reset and requests the initial value, empty query, and closed sheet. Reset never clears an external error.

Invalid ISO records, empty names, malformed dial codes, and duplicate ISO IDs are ignored; the first valid occurrence wins. Unknown value IDs fall back to the first enabled country. An empty country list renders a disabled selector and Continue action. Numbers are never silently shortened. Nonfinite/negative selection endpoints are clamped. Supplying a disabled country as the current value preserves its identity, but it cannot be chosen from the list.

Compatibility

Designed for Expo SDK 58, React 19, React Native 0.88.0, and Reanimated 4.7.1 with New Architecture. Supports iOS, Android, and web, including Expo Go with the matching SDK. No native directories or additional native dependency are required.

Compilation, isolated bundling, browser layout, and browser editing checks are separate from native touch, keyboard/IME behavior, screen reader operation, and hardware FPS measurements. No frame-rate guarantee is implied. Native text services and web composition use separate input adapters; a target IME must be exercised on its actual OS before shipping a localized phone flow.

Accessibility

Controls have at least 48-point targets, explicit labels, country identity and prefix, radio selection state, and a textual error. Number text stays LTR inside RTL layouts. Text remains scalable and long country names wrap. The form scrolls when space is constrained; the country list remains virtualized below a fixed header and search.

Reduced motion replaces sheet movement with a 200 ms fade and removes press scale. Changes to the native reduced-motion preference are observed. Browser keyboard controls include Tab/Shift+Tab within the open sheet, arrow navigation through mounted country rows, Enter to choose, and Escape to close. Native Back closes the sheet. Closing restores focus to the country control. Error announcements occur on message changes, never for inserted separators or animation frames. Every announcement and visible string is configurable.

The demo provides eight sample countries, one disabled entry (Japan), and two explicit fictional number fixtures: US/CA 2025550148 and GB 7700900148. Its validator is a fixture matcher, not production numbering data. System emoji flags are demo content with visible ISO fallback. Raster assets: N/A. Demo cloud icon: Lucide (ISC), copied from the repository kit; demo identity: Nimbus. No reference artwork is distributed.

Why this one

This field keeps country identity, dial code, national digits, formatting, and cursor selection together without guessing global validation rules. Search is single selection, errors reserve their space, and country changes preserve the input instance and national number. The contained sheet also fits embedded forms where a global modal would escape the host.