LanguageSwitcher
Compact language selector: a current-language trigger that fans out the other locales as a button group.
Status: stable. Group: navigation. Markdown version: /design-system/components/language-switcher.md
When to use
LanguageSwitcher is the site-chrome locale control: the current language shows as a compact button that fans out a tray of the other locales on demand. It is controlled; update your i18n instance in onLanguageChange and pass the new currentLang back. Each option's ariaLabel is the language's own name ("Suomi"), not an action phrase: the trigger and every option are named "<label>, <name>", and options carry lang so screen readers pronounce the name in that language. The closed tray is inert so hidden options can never take focus. Escape or an outside click closes it; tabbing out of the open tray does not.
When not to use
- Do not use flags as the only label; flags name countries, not languages.
Look-alikes
The contract names no look-alike with a deciding condition.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
languages | LanguageSwitcherOption[] | none | Available languages: code (also set as lang), visible label and ariaLabel, the language's own name used in the "<label>, <name>" accessible name. | Yes |
currentLang | string | none | Currently selected language code. | Yes |
onLanguageChange | (code: string) => void | none | Called with the selected language code. | Yes |
className | string | none | Optional classes on the group wrapper. | No |
buttonClassName | string | shrink-0 whitespace-nowrap px-2.5 py-1.5 text-sm font-heading font-semibold uppercase tracking-widest transition-colors motion-reduce:transition-none cursor-pointer rounded-sm border border-transparent bg-transparent text-muted-foreground hover:text-foreground hover:bg-foreground/5 | Base class override applied to every language button. | No |
activeButtonClassName | string | text-foreground hover:text-foreground | Class override for the current-language trigger. | No |
openTriggerClassName | string | bg-foreground/5 border-foreground text-foreground hover:border-foreground | Class override for the trigger while the tray is open. | No |
floatedButtonClassName | string | font-semibold text-foreground/80 hover:text-foreground | Class override for the fanned-out tray options. | No |
Variants
No variant dimensions and no props with a fixed set of values.
Invalid combinations
The contract records no invalid combinations.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the control and on to the next focusable element. (standard) |
| Enter | Activates the focused control. (standard) |
| Space | Activates or toggles the focused control. (standard) |
| Escape | Closes or dismisses the open layer. (standard) |
Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST | ARIA: Role=group with aria-label. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Trigger aria-expanded + aria-controls. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Each option is named '<label>, <language name>' (visible label first, WCAG 2.5.3) and carries lang. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Write each option's ariaLabel as the language's own name ("Suomi", "Svenska"), not an action phrase; the component builds the "<label>, <name>" accessible name from it. | Not stated in the contract. |
| SHOULD | Keep it controlled: change the i18n language inside onLanguageChange and feed the new currentLang back down. | Not stated in the contract. |
| SHOULD | Use short locale labels (EN/FI/SV); the fan tray is chrome, not a settings page. | Not stated in the contract. |
| SHOULD | Trust the built-in Escape, outside-click close, and inert tray; do not wrap it in another disclosure. | Not stated in the contract. |
| SHOULD NOT | Do not bury language choice in a settings modal on public pages; it is a first-visit need and belongs in the header. | Not stated in the contract. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { LanguageSwitcher } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/navigationexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-language-switcher>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.