Language Switcher
Copy-paste React language switcher with dropdown, search, RTL support and full accessibility. Works with next-intl, react-i18next and any i18n setup.
Installation
npx shadcn@latest add @babelize/language-switcherUsage
"use client";
import { useState } from "react";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
const locales = [{ code: "en" }, { code: "fr" }, { code: "es" }, { code: "ja" }, { code: "ar" }];
export function LocaleSwitcher() {
const [locale, setLocale] = useState("en");
return (
<LanguageSwitcher
locales={locales} // only `code` is required, labels and flags are auto-resolved
value={locale} // omit for uncontrolled, use defaultValue instead
onValueChange={setLocale}
label="native" // or "english" (default)
showFlags
/>
);
}Props
| Prop | Type | Required | Description |
|---|---|---|---|
locales | Locale[] | Yes | Array of available locales — only code is required |
value | string | No | Selected locale code — pass this to control the component |
defaultValue | string | No | Initial selected locale code (default: first locale's code) |
onValueChange | (code: string) => void | No | Callback when locale changes |
label | "native" | "english" | No | Display labels in native or English (default: "english") |
showFlags | boolean | No | Show flag emojis (default: false) |
maxHeight | number | string | No | Max height of the scrolling option list (default: "15rem") |
showCode | boolean | No | Show the locale code beside each option (default: false) |
variant | "outline" | "ghost" | "secondary" | No | Trigger style (default: "outline") |
size | "sm" | "default" | "lg" | No | Trigger size (default: "default") |
align | "start" | "end" | No | Edge of the trigger the menu lines up with (default: "start") |
side | "top" | "bottom" | No | Preferred menu side; flips when there is no room (default: "bottom") |
searchable | boolean | No | Show the search field (default: true) |
searchPlaceholder | string | No | Search placeholder (default: "Search languages...") |
emptyMessage | ReactNode | No | Shown when the search matches nothing (default: "No languages found.") |
labels | Partial<LanguageSwitcherLabels> | No | Override the built-in English UI text (see below) |
disabled | boolean | No | Disable the trigger |
renderOption | (locale, state) => ReactNode | No | Replace each option row's content; state has selected, active, label, flag |
classNames | LanguageSwitcherClassNames | No | Per-part classes: trigger, content, search, list, item, empty |
className | string | No | Additional CSS classes on the root |
Locale Object
| Property | Type | Description |
|---|---|---|
code | string | ISO 639-1 / BCP 47 code (e.g. "en", "fr-FR", "ar-SA") — required |
label | string | Override display name (auto-resolved from built-in mapping if omitted) |
flag | string | Override flag emoji (auto-detected from code if omitted) |
rtl | boolean | Override RTL (auto-detected from code if omitted) |
Examples
With Babelize SDK
Pair Language Switcher with the Babelize SDK for automatic translations:
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
import { useBabelize } from "@babelize/sdk/react";
const locales = [
{ code: "en", label: "English", flag: "🇺🇸" },
{ code: "fr", label: "Français", flag: "🇫🇷" },
{ code: "es", label: "Español", flag: "🇪🇸" },
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
{ code: "ja", label: "日本語", flag: "🇯🇵" },
{ code: "ar", label: "العربية", flag: "🇸🇦" },
];
export function LocaleSwitcher() {
const router = useRouter();
const { locale, setLocale } = useBabelize();
return (
<LanguageSwitcher
locales={locales}
value={locale}
onValueChange={(code) => {
setLocale(code);
router.push(`/${code}`);
}}
/>
);
}With next-intl
"use client";
import { useRouter } from "next/navigation";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
export function LocaleSwitcher() {
const router = useRouter();
return (
<LanguageSwitcher
locales={[{ code: "en" }, { code: "fr" }]}
defaultValue="en"
onValueChange={(code) => router.push(`/${code}`)}
/>
);
}With react-i18next
"use client";
import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
export function LocaleSwitcher() {
const { i18n } = useTranslation();
return (
<LanguageSwitcher
locales={[{ code: "en" }, { code: "fr" }]}
value={i18n.language}
onValueChange={(code) => i18n.changeLanguage(code)}
/>
);
}Native Labels + Flags
"use client";
import { LanguageSwitcher } from "@/components/ui/language-switcher";
export default function App() {
return (
<LanguageSwitcher
locales={[{ code: "en" }, { code: "fr" }, { code: "es" }]}
defaultValue="en"
label="native"
showFlags
/>
);
}Customizing parts
The switcher is styled with shadcn theme tokens, so it follows your theme. Use
classNames to restyle any part, or renderOption to change what each row shows:
<LanguageSwitcher
locales={locales}
variant="ghost"
size="sm"
align="end"
classNames={{ content: "w-64", item: "py-2" }}
renderOption={(locale, { label, flag, selected }) => (
<>
<span className="text-base leading-none">{flag}</span>
<span className="flex-1">{label}</span>
<span className="text-xs text-muted-foreground">{selected ? "Current" : locale.code}</span>
</>
)}
/>Translated UI text
The search placeholder, empty state and accessible 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 { LanguageSwitcher } from "@/components/ui/language-switcher";
export default function App() {
return (
<LanguageSwitcher
locales={[{ code: "en" }, { code: "fr" }, { code: "es" }]}
defaultValue="fr"
label="native"
labels={{
searchPlaceholder: "Rechercher une langue…",
searchLabel: "Rechercher une langue",
clearSearch: "Effacer la recherche",
emptyMessage: "Aucune langue trouvée.",
listbox: "Choisir la langue",
current: (name) => `Langue actuelle : ${name}`,
}}
/>
);
}| Key | Default | Used for |
|---|---|---|
searchPlaceholder | "Search languages..." | Search field placeholder |
searchLabel | "Search languages" | Search field accessible label |
clearSearch | "Clear search" | Clear button accessible label |
emptyMessage | "No languages found." | Shown when nothing matches |
listbox | "Select language" | Option list accessible label |
current | (name) => "Current language: " + name | Trigger accessible label |
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 locale highlighted |
ArrowDown / ArrowUp | Move the highlight, wrapping at both ends |
Home / End | Highlight the first / last locale |
Enter | Select the highlighted locale 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 |
The highlight stays on the row where it appeared when the menu opened — the
list scrolls beneath it, like a native menu — and the page itself never
scrolls. Pass maxHeight to change how tall the list gets before it scrolls.
Features
- Zero-config — just pass codes, labels and flags are auto-resolved
- Dropdown with search — filter locales by name or code
- RTL-aware — auto-detects RTL locales (ar, he, fa, ur) and adjusts layout
- Keyboard accessible — arrow keys, Home / End, Enter, and Escape navigation
- ARIA labels —
role="combobox"search field witharia-activedescendant, plusaria-expanded,aria-haspopup, andaria-selected - Dark mode — built with Tailwind CSS, dark-mode compatible
- Library agnostic — works with any i18n setup via callback
Supported Languages
Labels and flags are auto-detected from the language code. Pass just the code:
| Code | Language | Flag | RTL |
|---|---|---|---|
en | English | 🇺🇸 | |
fr | Français | 🇫🇷 | |
es | Español | 🇪🇸 | |
de | Deutsch | 🇩🇪 | |
ja | 日本語 | 🇯🇵 | |
ko | 한국어 | 🇰🇷 | |
zh | 中文 | 🇨🇳 | |
ar | العربية | 🇸🇦 | ✓ |
pt | Português | 🇧🇷 | |
it | Italiano | 🇮🇹 | |
nl | Nederlands | 🇳🇱 | |
ru | Русский | 🇷🇺 | |
pl | Polski | 🇵🇱 | |
tr | Türkçe | 🇹🇷 | |
vi | Tiếng Việt | 🇻🇳 | |
th | ไทย | 🇹🇭 | |
id | Bahasa Indonesia | 🇮🇩 | |
hi | हिन्दी | 🇮🇳 | |
bn | বাংলা | 🇧🇩 | |
uk | Українська | 🇺🇦 | |
cs | Čeština | 🇨🇿 | |
sv | Svenska | 🇸🇪 | |
da | Dansk | 🇩🇰 | |
fi | Suomi | 🇫🇮 | |
no | Norsk | 🇳🇴 | |
el | Ελληνικά | 🇬🇷 | |
he | עברית | 🇮🇱 | ✓ |
fa | فارسی | 🇮🇷 | ✓ |
ro | Română | 🇷🇴 | |
hu | Magyar | 🇭🇺 | |
sk | Slovenčina | 🇸🇰 | |
bg | Български | 🇧🇬 | |
hr | Hrvatski | 🇭🇷 | |
sr | Српски | 🇷🇸 | |
sl | Slovenščina | 🇸🇮 | |
lt | Lietuvių | 🇱🇹 | |
lv | Latviešu | 🇱🇻 | |
et | Eesti | 🇪🇪 | |
ga | Gaeilge | 🇮🇪 | |
ca | Català | 🇪🇸 | |
is | Íslenska | 🇮🇸 | |
sw | Kiswahili | 🇹🇿 | |
am | አማርኛ | 🇪🇹 | |
ne | नेपाली | 🇳🇵 | |
si | සිංහල | 🇱🇰 | |
my | မြန်မာ | 🇲🇲 | |
km | ខ្មែរ | 🇰🇭 | |
lo | ລາວ | 🇱🇦 | |
ka | ქართული | 🇬🇪 | |
hy | Հայերեն | 🇦🇲 | |
kk | Қазақ | 🇰🇿 | |
uz | Oʻzbek | 🇺🇿 | |
mn | Монгол | 🇲🇳 | |
ur | اردو | 🇵🇰 | ✓ |
ml | മലയാളം | 🇮🇳 | |
ta | தமிழ் | 🇮🇳 | |
te | తెలుగు | 🇮🇳 | |
kn | ಕನ್ನಡ | 🇮🇳 | |
mr | मराठी | 🇮🇳 | |
gu | ગુજરાતી | 🇮🇳 | |
pa | ਪੰਜਾਬੀ | 🇮🇳 | |
yo | Yorùbá | 🇳🇬 | |
ig | Igbo | 🇳🇬 | |
zu | isiZulu | 🇿🇦 | |
af | Afrikaans | 🇿🇦 | |
ha | Hausa | 🇳🇬 | |
tl | Filipino | 🇵🇭 | |
mi | Te Reo Māori | 🇳🇿 | |
sm | Gagana Samoa | 🇼🇸 | |
to | Lea Faka-Tonga | 🇹🇴 | |
eo | Esperanto | 🌐 |
Need a language not listed? Pass a custom label and flag to override:
<LanguageSwitcher
locales={[{ code: "xyz", label: "My Language", flag: "🏴" }]}
defaultValue="xyz"
/>Dependencies
This component uses:
clsxandtailwind-merge(bundled as dependencies)
Peer dependencies:
react>= 18react-dom>= 18tailwindcss>= 3
Credits
Created by sapatmohit.