Phone Input
Phone number input with country code selector.
Installation#
npx @aniui/cli add phone-inputWeb preview — components render natively on iOS & Android
import { PhoneInput } from "@/components/ui/phone-input";
export function MyScreen() {
return (
<PhoneInput
defaultCountry="US"
onChangeText={(phone, dial, raw) => {
console.log(phone); // "+15551234567"
console.log(dial); // "+1"
console.log(raw); // "5551234567"
}}
/>
);
}Usage#
app/index.tsx
import { PhoneInput } from "@/components/ui/phone-input";
export function MyScreen() {
return (
<PhoneInput
defaultCountry="US"
onChangeText={(phone, dial, raw) => {
console.log(phone); // "+15551234567"
console.log(dial); // "+1"
console.log(raw); // "5551234567"
}}
/>
);
}Props#
PropTypeDefault
variant"default" | "ghost""default"size"sm" | "md" | "lg""md"defaultCountrystring"US"onChangeText(phone: string, dial: string, raw: string) => void-classNamestring-Also accepts all TextInput props except onChangeText.
Accessibility#
- Phone number input with country code picker.
- Country selector and input field are separately focusable for screen readers.
Source#
components/ui/phone-input.tsx
import React, { useState, useCallback } from "react";
import { View, TextInput, Pressable, Text, ScrollView, Modal } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const phoneVariants = cva("flex-row items-center rounded-md border", {
variants: {
variant: {
default: "border-input bg-background",
ghost: "border-transparent bg-transparent",
},
size: {
sm: "min-h-9 px-3",
md: "min-h-12 px-4",
lg: "min-h-14 px-5",
},
},
defaultVariants: { variant: "default", size: "md" },
});
type Country = { code: string; dial: string; name: string };
const countries: Country[] = [
{ code: "US", dial: "+1", name: "United States" },
{ code: "GB", dial: "+44", name: "United Kingdom" },
{ code: "IN", dial: "+91", name: "India" },
{ code: "CA", dial: "+1", name: "Canada" },
{ code: "AU", dial: "+61", name: "Australia" },
{ code: "DE", dial: "+49", name: "Germany" },
{ code: "FR", dial: "+33", name: "France" },
{ code: "JP", dial: "+81", name: "Japan" },
{ code: "BR", dial: "+55", name: "Brazil" },
{ code: "MX", dial: "+52", name: "Mexico" },
];
export interface PhoneInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof TextInput>, "onChangeText">,
VariantProps<typeof phoneVariants> {
className?: string;
defaultCountry?: string;
onChangeText?: (phone: string, dial: string, raw: string) => void;
}
export function PhoneInput({
variant,
size,
className,
defaultCountry = "US",
onChangeText,
...props
}: PhoneInputProps) {
const [country, setCountry] = useState(countries.find((c) => c.code === defaultCountry) ?? countries[0]);
const [open, setOpen] = useState(false);
const handleChange = useCallback(
(text: string) => {
const raw = text.replace(/\D/g, "");
onChangeText?.(`${country.dial}${raw}`, country.dial, raw);
},
[country, onChangeText]
);
return (
<View className={cn(phoneVariants({ variant, size }), className)}>
<Pressable
onPress={() => setOpen(true)}
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Country: ${country.name}`}
className="flex-row items-center mr-2 pr-2 border-r border-border min-h-8"
>
<Text className="text-foreground text-base">{country.dial}</Text>
<Text className="text-muted-foreground ml-1 text-xs">▼</Text>
</Pressable>
<TextInput
className="flex-1 text-foreground p-0 text-base"
placeholderTextColor="hsl(240 3.8% 46.1%)"
keyboardType="phone-pad"
onChangeText={handleChange}
placeholder="Phone number"
{...props}
/>
<Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
<Pressable className="flex-1 bg-black/50 justify-end" onPress={() => setOpen(false)}>
<View className="bg-card rounded-t-2xl max-h-80 pb-8">
<View className="items-center py-3">
<View className="w-10 h-1 rounded-full bg-muted" />
</View>
<ScrollView>
{countries.map((c) => (
<Pressable
key={c.code}
className={cn("flex-row items-center px-5 py-3", c.code === country.code && "bg-accent")}
onPress={() => { setCountry(c); setOpen(false); }}
accessibilityRole="button"
>
<Text className="text-foreground flex-1">{c.name}</Text>
<Text className="text-muted-foreground">{c.dial}</Text>
</Pressable>
))}
</ScrollView>
</View>
</Pressable>
</Modal>
</View>
);
}