Locale Picker
Region picker with locale-derived defaults — currency, date and number formats, and first day of week. Searchable, keyboard accessible, and RTL-aware.
- Currency
- INR₹12,34,567.89
- Date format
- 15 Jan 2026
- Number format
- 12,34,567.89
- Week starts on
- Sunday
Installation
npx shadcn@latest add @babelize/locale-pickerUsage
import { LocalePicker } from "@/components/ui/locale-picker";<LocalePicker
regions={["IN", "US", "GB", "DE", "JP"]} // your ISO 3166-1 alpha-2 regions
defaultValue="en-IN"
onValueChange={(locale) => console.log("Selected locale:", locale)}
label="english" // native, english
variant="outline" // outline, ghost, secondary
size="default" // sm, default, lg
/>Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
regions | string[] | Yes | — | Regions to choose from, ISO 3166-1 alpha-2 codes |
value | string | No | — | Selected locale (BCP 47) — pass this to control the component |
defaultValue | string | No | en-<first region> | Initial selected locale when uncontrolled |
onValueChange | (locale: string) => void | No | — | Callback when the region changes — receives the new BCP 47 tag |
label | "native" | "english" | No | "english" | Region names in English, or localized to the selected locale |
showFlags | boolean | No | true | Show the region's flag emoji next to its name |
maxHeight | number | string | No | "15rem" | Max height of the scrolling option list, before it scrolls |
variant | "outline" | "ghost" | "secondary" | No | "outline" | Trigger style |
size | "sm" | "default" | "lg" | No | "default" | Trigger size |
align | "start" | "end" | No | "start" | Edge of the trigger the popup lines up with |
side | "top" | "bottom" | No | "bottom" | Preferred popup side — flips when the viewport has no room |
searchable | boolean | No | true | Show the search field |
searchPlaceholder | string | No | "Search regions..." | Placeholder for the search field |
emptyMessage | React.ReactNode | No | "No results" | Content shown when the search matches nothing |
disabled | boolean | No | false | Disable the trigger |
showSummary | boolean | No | true | Show the summary card of derived locale defaults |
labels | Partial<LocalePickerLabels> | No | — | Override the built-in English UI text (see below) |
classNames | LocalePickerClassNames | No | — | Per-part classes: trigger, content, search, list, item, empty, summary |
className | string | No | — | Additional CSS classes |
The selected value is a full BCP 47 tag: its region subtag is the picked
region, and its language subtag survives region changes. Picking Germany
from en-IN yields en-DE; picking it from fr-JP yields fr-DE.
Derived defaults
The summary card derives everything from the selected locale through Intl —
no data bundled beyond an ISO 4217 fallback table for the currency code:
| Row | Source |
|---|---|
| Currency | Region → ISO 4217 code, previewed with Intl.NumberFormat (style: "currency") |
| Date format | Intl.DateTimeFormat (dateStyle: "medium") on a fixed sample date |
| Number format | Intl.NumberFormat on a fixed sample amount |
| Week starts on | Intl.Locale#getWeekInfo() where available, with a regional fallback |
Regions without a currency mapping show an em dash rather than a guess.
Keyboard support
The popup follows the WAI-ARIA combobox pattern: focus stays in the search
field, which is exposed as a role="combobox", and the highlighted option is
announced through aria-activedescendant.
| Key | Action |
|---|---|
Enter, Space, or click | Open the popup from the trigger |
ArrowDown / ArrowUp (closed) | Open with the first / last region highlighted |
ArrowDown / ArrowUp | Move the highlight, wrapping at both ends |
Home / End | Highlight the first / last region |
Enter | Select the highlighted region and return focus to the trigger |
Escape | Close the popup and return focus to the trigger |
| Typing | Filter by name or code; the highlight restarts at the top |
Features
- Derived defaults — currency, date format, number format, and first day of week, resolved live through
Intl - Dropdown with search — filter regions by English name, localized name, or region code, with a clear button
- Motion — the popup fades and slides via
motion/react, honouringprefers-reduced-motion, and leaves the accessibility tree as it closes - RTL-aware — flips direction for RTL locales (ar, he, fa, ur) via
diron the root - Keyboard accessible — arrow keys, Home / End, Enter, and Escape navigation
- ARIA labels —
role="combobox"search field witharia-activedescendant, plusaria-expanded,aria-haspopup, andaria-selected - Themed with shadcn tokens —
bg-popover,border-input,bg-accentand friends, so it inherits your theme; dark mode comes from the tokens - Customisable —
variant/sizetriggers plus per-partclassNames(LocalePickerClassNamesis exported from the barrel), withlucide-reacticons throughout - Controlled or uncontrolled —
value/defaultValue/onValueChange
Examples
Controlled
<LocalePicker regions={["IN", "US", "GB", "DE", "JP"]} value={locale} onValueChange={setLocale} />Native region names
<LocalePicker regions={["IN", "US", "GB", "DE", "JP"]} defaultValue="de-DE" label="native" />With de-DE selected, the trigger reads "Deutschland" and the options read
"Vereinigte Staaten", "Vereinigtes Königreich", and so on.
Translated UI text
The search placeholder, empty state, accessible labels and summary row labels
default to English. Pass labels to translate them — any key you leave out
keeps its English default. The searchPlaceholder and emptyMessage props
still work, and win over labels when both are set.
"use client";
import { LocalePicker } from "@/components/ui/locale-picker";
export default function App() {
return (
<LocalePicker
regions={["IN", "US", "GB", "DE", "JP"]}
defaultValue="en-IN"
labels={{
searchPlaceholder: "Rechercher une région…",
searchLabel: "Rechercher une région",
clearSearch: "Effacer la recherche",
emptyMessage: "Aucune région trouvée.",
listbox: "Choisir la région",
current: (name) => `Région actuelle : ${name}`,
currency: "Devise",
dateFormat: "Format de date",
numberFormat: "Format de nombre",
weekStart: "La semaine commence le",
}}
/>
);
}Dependencies
This component uses:
clsxandtailwind-merge(bundled as dependencies)lucide-react(icons)motion(popup animation)
Peer dependencies:
react>= 18react-dom>= 18tailwindcss>= 3
Credits
Created by ujwalbajaj.