Shadcn Phone Input
An implementation of a Phone Input component for React, built on top of Shadcn UI's input component.
Setup
Install Shadcn via CLI
Run the shadcn-ui
init command to setup your project:
npx shadcn-ui@latest init
Install necessary Shadcn components:
Run the shadcn-ui
add command to add the necessary shadcn components to your project:
npx shadcn-ui@latest add input npx shadcn-ui@latest add button npx shadcn-ui@latest add command npx shadcn-ui@latest add toast npx shadcn-ui@latest add popover npx shadcn-ui@latest add scroll-area
Install necessary React Phone Number Input package:
npm install react-phone-number-input
To use the phone input component:
Snippets
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import * as RPNInput from "react-phone-number-input";
import flags from "react-phone-number-input/flags";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input, InputProps } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { ScrollArea } from "./scroll-area";
type PhoneInputProps = Omit<
React.ComponentProps<"input">,
"onChange" | "value" | "ref"
> &
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
onChange?: (value: RPNInput.Value) => void;
};
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
({ className, onChange, ...props }, ref) => {
return (
<RPNInput.default
ref={ref}
className={cn("flex", className)}
flagComponent={FlagComponent}
countrySelectComponent={CountrySelect}
inputComponent={InputComponent}
smartCaret={false}
/**
* Handles the onChange event.
*
* react-phone-number-input might trigger the onChange event as undefined
* when a valid phone number is not entered. To prevent this,
* the value is coerced to an empty string.
*
* @param {E164Number | undefined} value - The entered value
*/
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
{...props}
/>
);
},
);
PhoneInput.displayName = "PhoneInput";
const InputComponent = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => (
<Input
className={cn("rounded-e-lg rounded-s-none", className)}
{...props}
ref={ref}
/>
),
);
InputComponent.displayName = "InputComponent";
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
type CountrySelectProps = {
disabled?: boolean;
value: RPNInput.Country;
options: CountryEntry[];
onChange: (country: RPNInput.Country) => void;
};
const CountrySelect = ({
disabled,
value: selectedCountry,
options: countryList,
onChange,
}: CountrySelectProps) => {
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
className="flex gap-1 rounded-e-none rounded-s-lg px-3 border-r-0 focus:z-10"
disabled={disabled}
>
<FlagComponent
country={selectedCountry}
countryName={selectedCountry}
/>
<ChevronsUpDown
className={cn(
"-mr-2 size-4 opacity-50",
disabled ? "hidden" : "opacity-100",
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search country..." />
<CommandList>
<ScrollArea className="h-72">
<CommandEmpty>No country found.</CommandEmpty>
<CommandGroup>
{countryList.map(({ value, label }) =>
value ? (
<CountrySelectOption
key={value}
country={value}
countryName={label}
selectedCountry={selectedCountry}
onChange={onChange}
/>
) : null,
)}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
interface CountrySelectOptionProps extends RPNInput.FlagProps {
selectedCountry: RPNInput.Country;
onChange: (country: RPNInput.Country) => void;
}
const CountrySelectOption = ({
country,
countryName,
selectedCountry,
onChange,
}: CountrySelectOptionProps) => {
return (
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
<FlagComponent country={country} countryName={countryName} />
<span className="flex-1 text-sm">{countryName}</span>
<span className="text-foreground/50 text-sm">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
<CheckIcon
className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
/>
</CommandItem>
);
};
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
const Flag = flags[country];
return (
<span className="bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm">
{Flag && <Flag title={countryName} />}
</span>
);
};
export { PhoneInput };
Variants
With country
The phone input component can be used with a country select dropdown.
Summary
Setting default country
Internationalization
Force international format
Force national format
initialValueFormat
Formatting value
National:
International:
Country code: