An ISO 3166 compliant dropdown component for selecting a country.
>_
The CountryDropdown
component uses the Popover
and Command
components from the shadcn/ui library.
The CountryDropdown
is built using react-circle-flags and country-data-list packages.
The country-data-list package provides both ISO 3166-1 alpha-2 and ISO 3166-1 alpha-3 country codes. Select an country from the demo above to see the full country object.
Install the country-dropdown
package using your preferred package manager:
# Install dependencies
pnpm add react-circle-flags country-data-list
# Install shadcn components
npx shadcn@latest add button
npx shadcn@latest add command
npx shadcn@latest add popover
Copy the component below to components/ui/country-dropdown.tsx
.
"use client";
import React, { useCallback, useState, forwardRef, useEffect } from "react";
// shadcn
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
// utils
import { cn } from "@/lib/utils";
// assets
import { ChevronDown, CheckIcon, Globe } from "lucide-react";
import { CircleFlag } from "react-circle-flags";
// data
import { countries } from "country-data-list";
// Country interface
export interface Country {
alpha2: string;
alpha3: string;
countryCallingCodes: string[];
currencies: string[];
emoji?: string;
ioc: string;
languages: string[];
name: string;
status: string;
}
// Dropdown props
interface CountryDropdownProps {
options?: Country[];
onChange?: (country: Country) => void;
defaultValue?: string;
disabled?: boolean;
placeholder?: string;
slim?: boolean;
}
const CountryDropdownComponent = (
{
options = countries.all.filter(
(country: Country) =>
country.emoji && country.status !== "deleted" && country.ioc !== "PRK"
),
onChange,
defaultValue,
disabled = false,
placeholder = "Select a country",
slim = false,
...props
}: CountryDropdownProps,
ref: React.ForwardedRef<HTMLButtonElement>
) => {
const [open, setOpen] = useState(false);
const [selectedCountry, setSelectedCountry] = useState<Country | undefined>(
undefined
);
useEffect(() => {
if (defaultValue) {
const initialCountry = options.find(
(country) => country.alpha3 === defaultValue
);
if (initialCountry) {
setSelectedCountry(initialCountry);
} else {
// Reset selected country if defaultValue is not found
setSelectedCountry(undefined);
}
} else {
// Reset selected country if defaultValue is undefined or null
setSelectedCountry(undefined);
}
}, [defaultValue, options]);
const handleSelect = useCallback(
(country: Country) => {
console.log("🌍 CountryDropdown value: ", country);
setSelectedCountry(country);
onChange?.(country);
setOpen(false);
},
[onChange]
);
const triggerClasses = cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
slim === true && "w-20"
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
ref={ref}
className={triggerClasses}
disabled={disabled}
{...props}
>
{selectedCountry ? (
<div className="flex items-center flex-grow w-0 gap-2 overflow-hidden">
<div className="inline-flex items-center justify-center w-5 h-5 shrink-0 overflow-hidden rounded-full">
<CircleFlag
countryCode={selectedCountry.alpha2.toLowerCase()}
height={20}
/>
</div>
{slim === false && (
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{selectedCountry.name}
</span>
)}
</div>
) : (
<span>
{slim === false ? (
placeholder || setSelectedCountry.name
) : (
<Globe size={20} />
)}
</span>
)}
<ChevronDown size={16} />
</PopoverTrigger>
<PopoverContent
collisionPadding={10}
side="bottom"
className="min-w-[--radix-popper-anchor-width] p-0"
>
<Command className="w-full max-h-[200px] sm:max-h-[270px]">
<CommandList>
<div className="sticky top-0 z-10 bg-popover">
<CommandInput placeholder="Search country..." />
</div>
<CommandEmpty>No country found.</CommandEmpty>
<CommandGroup>
{options
.filter((x) => x.name)
.map((option, key: number) => (
<CommandItem
className="flex items-center w-full gap-2"
key={key}
onSelect={() => handleSelect(option)}
>
<div className="flex flex-grow w-0 space-x-2 overflow-hidden">
<div className="inline-flex items-center justify-center w-5 h-5 shrink-0 overflow-hidden rounded-full">
<CircleFlag
countryCode={option.alpha2.toLowerCase()}
height={20}
/>
</div>
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{option.name}
</span>
</div>
<CheckIcon
className={cn(
"ml-auto h-4 w-4 shrink-0",
option.name === selectedCountry?.name
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
CountryDropdownComponent.displayName = "CountryDropdownComponent";
export const CountryDropdown = forwardRef(CountryDropdownComponent);
A slim version makes it easy for you to have a shorthand version displaying only the country flag. This could be used in conjunction with internationalisation.
>_
The multiple country selector is an additional option that returns a string array. When using in conjunction with Zod schema make sure to always use a string array; z.array(z.string())
. Also provide an empty array in the defaultValue
prop and
>_
Easily use the CountryDropdown
with shadcn/ui forms.
>_