Babelize Elements
Language Switchers

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-switcher

Usage

"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

PropTypeRequiredDescription
localesLocale[]YesArray of available locales — only code is required
valuestringNoSelected locale code — pass this to control the component
defaultValuestringNoInitial selected locale code (default: first locale's code)
onValueChange(code: string) => voidNoCallback when locale changes
label"native" | "english"NoDisplay labels in native or English (default: "english")
showFlagsbooleanNoShow flag emojis (default: false)
maxHeightnumber | stringNoMax height of the scrolling option list (default: "15rem")
showCodebooleanNoShow the locale code beside each option (default: false)
variant"outline" | "ghost" | "secondary"NoTrigger style (default: "outline")
size"sm" | "default" | "lg"NoTrigger size (default: "default")
align"start" | "end"NoEdge of the trigger the menu lines up with (default: "start")
side"top" | "bottom"NoPreferred menu side; flips when there is no room (default: "bottom")
searchablebooleanNoShow the search field (default: true)
searchPlaceholderstringNoSearch placeholder (default: "Search languages...")
emptyMessageReactNodeNoShown when the search matches nothing (default: "No languages found.")
labelsPartial<LanguageSwitcherLabels>NoOverride the built-in English UI text (see below)
disabledbooleanNoDisable the trigger
renderOption(locale, state) => ReactNodeNoReplace each option row's content; state has selected, active, label, flag
classNamesLanguageSwitcherClassNamesNoPer-part classes: trigger, content, search, list, item, empty
classNamestringNoAdditional CSS classes on the root

Locale Object

PropertyTypeDescription
codestringISO 639-1 / BCP 47 code (e.g. "en", "fr-FR", "ar-SA") — required
labelstringOverride display name (auto-resolved from built-in mapping if omitted)
flagstringOverride flag emoji (auto-detected from code if omitted)
rtlbooleanOverride 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}`,
      }}
    />
  );
}
KeyDefaultUsed 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: " + nameTrigger 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.

KeyAction
Enter, Space, or clickOpen the popup from the trigger
ArrowDown / ArrowUp (closed)Open with the first / last locale highlighted
ArrowDown / ArrowUpMove the highlight, wrapping at both ends
Home / EndHighlight the first / last locale
EnterSelect the highlighted locale and return focus to the trigger
EscapeClose the popup and return focus to the trigger
TypingFilter 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 with aria-activedescendant, plus aria-expanded, aria-haspopup, and aria-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:

CodeLanguageFlagRTL
enEnglish🇺🇸
frFrançais🇫🇷
esEspañol🇪🇸
deDeutsch🇩🇪
ja日本語🇯🇵
ko한국어🇰🇷
zh中文🇨🇳
arالعربية🇸🇦✓
ptPortuguês🇧🇷
itItaliano🇮🇹
nlNederlands🇳🇱
ruРусский🇷🇺
plPolski🇵🇱
trTürkçe🇹🇷
viTiếng Việt🇻🇳
thไทย🇹🇭
idBahasa Indonesia🇮🇩
hiहिन्दी🇮🇳
bnবাংলা🇧🇩
ukУкраїнська🇺🇦
csČeština🇨🇿
svSvenska🇸🇪
daDansk🇩🇰
fiSuomi🇫🇮
noNorsk🇳🇴
elΕλληνικά🇬🇷
heעברית🇮🇱✓
faفارسی🇮🇷✓
roRomână🇷🇴
huMagyar🇭🇺
skSlovenčina🇸🇰
bgБългарски🇧🇬
hrHrvatski🇭🇷
srСрпски🇷🇸
slSlovenščina🇸🇮
ltLietuvių🇱🇹
lvLatviešu🇱🇻
etEesti🇪🇪
gaGaeilge🇮🇪
caCatalà🇪🇸
isÍslenska🇮🇸
swKiswahili🇹🇿
amአማርኛ🇪🇹
neनेपाली🇳🇵
siසිංහල🇱🇰
myမြန်မာ🇲🇲
kmខ្មែរ🇰🇭
loລາວ🇱🇦
kaქართული🇬🇪
hyՀայերեն🇦🇲
kkҚазақ🇰🇿
uzOʻzbek🇺🇿
mnМонгол🇲🇳
urاردو🇵🇰✓
mlമലയാളം🇮🇳
taதமிழ்🇮🇳
teతెలుగు🇮🇳
knಕನ್ನಡ🇮🇳
mrमराठी🇮🇳
guગુજરાતી🇮🇳
paਪੰਜਾਬੀ🇮🇳
yoYorùbá🇳🇬
igIgbo🇳🇬
zuisiZulu🇿🇦
afAfrikaans🇿🇦
haHausa🇳🇬
tlFilipino🇵🇭
miTe Reo Māori🇳🇿
smGagana Samoa🇼🇸
toLea Faka-Tonga🇹🇴
eoEsperanto🌐

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:

  • clsx and tailwind-merge (bundled as dependencies)

Peer dependencies:

  • react >= 18
  • react-dom >= 18
  • tailwindcss >= 3

Credits

Created by sapatmohit.

On this page