Phone Input
An implementation of a Phone Input component for React, built on top of Shadcn UI input component.
An implementation of a Phone Input component for React, built on top of Shadcn UI input component.
Read the Docs
npm install @hookform/resolvers/zod react-hook-form react-phone-number-input
1import { CheckIcon, ChevronsUpDown } from 'lucide-react';2import * as React from 'react';3import * as RPNInput from 'react-phone-number-input';4import flags from 'react-phone-number-input/flags';5import { Button } from '@/components/website/ui/button';6import {7Command,8CommandEmpty,9CommandGroup,10CommandInput,11CommandItem,12CommandList,13} from '@/components/website/ui/command';14import {15Popover,16PopoverContent,17PopoverTrigger,18} from '@/components/website/ui/popover';1920import { cn } from '@/lib/utils';21import { ScrollArea } from '@/components/website/ui/scroll-area';2223type PhoneInputProps = Omit<24React.InputHTMLAttributes<HTMLInputElement>,25'onChange' | 'value'26> &27Omit<RPNInput.Props<typeof RPNInput.default>, 'onChange'> & {28onChange?: (value: RPNInput.Value) => void;29};30const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =31React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(32({ className, onChange, ...props }, ref) => {33return (34<RPNInput.default35ref={ref}36className={cn('flex', className)}37flagComponent={FlagComponent}38countrySelectComponent={CountrySelect}39inputComponent={InputComponent}40/**41* Handles the onChange event.42*43* react-phone-number-input might trigger the onChange event as undefined44* when a valid phone number is not entered. To prevent this,45* the value is coerced to an empty string.46*47* @param {E164Number | undefined} value - The entered value48*/49// @ts-ignore50onChange={(value) => onChange?.(value || '')}51{...props}52/>53);54}55);56PhoneInput.displayName = 'PhoneInput';5758const InputComponent = React.forwardRef<HTMLInputElement, any>(59({ className, ...props }, ref) => (60<input61className={cn(62'rounded-e-lg rounded-s-none px-2 bg-background outline-none ',63className64)}65{...props}66ref={ref}67/>68)69);70InputComponent.displayName = 'InputComponent';7172type CountrySelectOption = { label: string; value: RPNInput.Country };7374type CountrySelectProps = {75disabled?: boolean;76value: RPNInput.Country;77onChange: (value: RPNInput.Country) => void;78options: CountrySelectOption[];79};8081const CountrySelect = ({82disabled,83value,84onChange,85options,86}: CountrySelectProps) => {87const handleSelect = React.useCallback(88(country: RPNInput.Country) => {89onChange(country);90},91[onChange]92);9394return (95<Popover>96<PopoverTrigger asChild>97<Button98type='button'99variant={'outline'}100className={cn('flex gap-1 rounded-e-none rounded-s-lg px-3')}101disabled={disabled}102>103<FlagComponent country={value} countryName={value} />104<ChevronsUpDown105className={cn(106'-mr-2 h-4 w-4 opacity-50',107disabled ? 'hidden' : 'opacity-100'108)}109/>110</Button>111</PopoverTrigger>112<PopoverContent className='w-[300px] p-0'>113<Command>114<CommandList>115<ScrollArea className='h-72'>116<CommandInput placeholder='Search country...' />117<CommandEmpty>No country found.</CommandEmpty>118<CommandGroup>119{options120.filter((x) => x.value)121.map((option) => (122<CommandItem123className='gap-2'124key={option.value}125onSelect={() => handleSelect(option.value)}126>127<FlagComponent128country={option.value}129countryName={option.label}130/>131<span className='flex-1 text-sm'>{option.label}</span>132{option.value && (133<span className='text-foreground/50 text-sm'>134{`+${RPNInput.getCountryCallingCode(option.value)}`}135</span>136)}137<CheckIcon138className={cn(139'ml-auto h-4 w-4',140option.value === value ? 'opacity-100' : 'opacity-0'141)}142/>143</CommandItem>144))}145</CommandGroup>146</ScrollArea>147</CommandList>148</Command>149</PopoverContent>150</Popover>151);152};153154const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {155const Flag = flags[country];156157return (158<span className='bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm'>159{Flag && <Flag title={countryName} />}160</span>161);162};163FlagComponent.displayName = 'FlagComponent';164165export { PhoneInput };