Language Switcher — Locale Dropdown HTML CSS JS

Language Switcher · Navigation · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Flag + English + native name per locale
Each option shows its flag, English name, and endonym (Deutsch, 日本語) so visitors recognise their own language instantly.
Type-to-filter search
The search box filters on every keystroke against both the English and native names, with an instant empty state.
Full keyboard navigation
Arrow keys move the highlight (with scrollIntoView), Enter selects, Escape closes, and the search auto-focuses on open.
Clear selected state
The current language shows a checkmark and bold weight in the list, and its flag/name appear on the trigger.
Outside-click and Escape dismissal
A document-level listener and the Escape key both close the dropdown, the standard popover pattern.
Accessible listbox semantics
aria-haspopup, aria-expanded, role="listbox", and role="option" make it a real listbox for assistive tech.
Sets document.lang on select
Selecting updates document.documentElement.lang — the hook to extend into real routing, dictionary swaps, or a locale cookie.
Animation-safe dropdown
Opacity and transform-only transitions keep the open/close smooth across every framework export including Tailwind.

About this UI Snippet

Language Switcher — Searchable Locale Dropdown with Flags & Keyboard Navigation

Screenshot of the Language Switcher snippet rendered live

If your site serves more than one country, a language switcher is one of the first things international visitors look for — and a long, unsearchable list of locales is one of the most common ways to get it wrong. This snippet builds a polished, searchable language-picker dropdown in plain HTML, CSS, and vanilla JavaScript: flag icons, English *and* native names, a type-to-filter search, full keyboard navigation, and a clear selected-state checkmark — everything a production i18n control needs, with no library.

Why native names matter

Each language shows both its English name and its endonym — the name in its own language ("German / Deutsch", "Japanese / 日本語", "中文"). This is the single most important usability detail in a language switcher: a visitor who can't read the current interface language is looking for *their* language written the way they'd recognise it, not the English label. Showing both covers the user choosing from an unfamiliar UI and the admin scanning an English list.

Search that filters both names

With a dozen or more locales, scrolling is slower than typing. The search box filters the list on every keystroke against both the English name and the native name, so "esp", "español", or "spanish" all surface Spanish. A focused-state border and an instant empty state ("No languages match") keep the control responsive and honest about no-result queries.

Full keyboard support

Opening the dropdown auto-focuses the search field, and Arrow Down/Up move an activeIndex highlight through the visible results with scrollIntoView({ block: 'nearest' }) so the highlighted row never leaves view. Enter selects the highlighted language, Escape closes the dropdown, and a document-level click listener closes it on an outside click. This mirrors the native <select> and combobox behaviour keyboard and screen-reader users already expect, which a styled <div> dropdown otherwise throws away.

Wiring it to real localization

Selecting a language updates the trigger's flag and label and sets document.documentElement.lang — the hook a real app extends. In practice you'd do one of three things on select: set the <html lang> and swap an in-memory translation dictionary (client-side i18n like i18next or vue-i18n), navigate to the locale-prefixed URL (/es/, /fr/ — the SEO-friendly approach search engines prefer), or set a locale cookie and reload. The component doesn't assume which; it exposes the choice and leaves the routing to you.

Accessibility and markup

The trigger carries aria-haspopup="listbox" and a toggled aria-expanded, the popup is role="listbox", and each option is role="option" — so assistive tech announces it as a real listbox rather than a mystery widget. The dropdown animates in with opacity and transform only (never height), keeping it smooth across every framework export.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA "English 🇺🇸" trigger button renders. Click it to open the searchable language dropdown.
  2. 2
    Search for a languageType in the search box — the list filters live against both English and native names ("español" or "spanish" both find Spanish).
  3. 3
    Navigate with the keyboardUse ArrowDown/ArrowUp to highlight a language, Enter to select it, or Escape to close — the search field is auto-focused on open.
  4. 4
    Select a languageClick or press Enter — the trigger updates to the chosen flag and name, a checkmark marks it selected, and document.lang is set.
  5. 5
    Edit the language listAdd or remove entries in the LANGS array ({ code, flag, name, native }) to match the locales your site actually supports.
  6. 6
    Wire up real localizationIn the select() function, navigate to the locale-prefixed URL (/es/, /fr/), swap your translation dictionary, or set a locale cookie and reload.

Real-world uses

Common Use Cases

Multilingual marketing sites
The locale picker in a site header for any product sold across countries — pair with a country selector for shipping/region fields.
SaaS app settings
Let users set their interface language in account settings, persisting the choice to their profile.
Documentation and help centers
Switch between translated versions of docs, with the URL navigating to the locale-prefixed path for SEO.
E-commerce storefronts
Combine a language switcher with a currency control (see the currency converter) for full internationalization.
Travel and booking platforms
A searchable locale list is essential where the audience spans many languages and visitors arrive in an unfamiliar UI.
Learning accessible dropdown patterns
A practical reference for combobox keyboard navigation and listbox ARIA — compare with a multi-select dropdown for the multi-pick case.

Got questions?

Frequently Asked Questions

In the select() function, do one of three things: navigate to a locale-prefixed URL (/es/path, the most SEO-friendly), call your client i18n library to swap the active translation dictionary (i18next, vue-i18n, next-intl), or set a locale cookie and reload so the server renders the right language. The component already sets document.documentElement.lang for you as the starting hook.

For public, indexable content prefer distinct locale URLs (/en/, /es/) so each language is its own crawlable page with hreflang tags — cookies hide the translation from search engines. For an authenticated app where SEO is irrelevant, a stored user preference or cookie is simpler and avoids URL clutter.

Read navigator.language (or navigator.languages for the ordered list) on first visit, match its prefix against your LANGS codes, and pre-select that language — but always let the user override it and remember their explicit choice, since the browser locale is a hint, not a decision.

Emoji flags render on most platforms but not all (notably some Windows versions show two-letter codes instead). For full consistency, replace the emoji with small SVG or PNG flag icons, or drop flags entirely and rely on the native names — flags also imperfectly map to languages (Spanish isn't only Spain), so native names are the more robust signal.

In React, hold the current code and query in useState and derive the filtered list with useMemo, calling your i18n library or router in the select handler; in Vue, use ref()/computed() and vue-i18n; in Angular, use a component field and ngx-translate or the built-in i18n. The keyboard navigation and outside-click logic port directly.