Zetta v1.6

Components · App chrome

Language switcher

A menu-triggered control for choosing the interface language, listing the supported set and marking the current one. New in v1.5. It is UI only — no i18n runtime is implied, and localization and RTL specifics remain an open gap in the spec.

Preview

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
Apply the Zetta "Language switcher" component to this project using the zetta-design-md skill.

WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `language-switcher` spec (variants: trigger, menu, item, item-active) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `language-switcher` spec in this project's stack.

AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.

ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility.

Anatomy & rules

Trigger (in header):
  🌐 English  ⌄            ← language-switcher-trigger (globe + current language + chevron)

Open menu:
  ╭─────────────────────────╮
  │ ✓  English              │  ← language-switcher-item-active (aria-current="true")
  │    Français             │  ← language-switcher-item
  │    Español              │
  │    Bahasa Indonesia     │
  ╰─────────────────────────╯   ← language-switcher-menu
State Visual
Closed Trigger at rest, showing the current language label
Trigger hover {colors.primary-bg} fill, {colors.primary} text
Trigger focus Focus ring ({colors.border-focus})
Open language-switcher-menu panel visible, languages listed
Item hover {colors.primary-bg} fill on hovered language
Item focus Focus outline on the language item
Active language {colors.primary-bg} fill, check icon prefix, aria-current="true"

When to use

  • Global language selection in the app header or website navbar.
  • Any surface where the user should be able to change the interface language.

When not to use

  • Selecting a value for a form field (e.g. a "preferred language" setting inside a form) — use Select.
  • Switching between app themes — use Theme Switcher.

Do

  • Show the current language on the trigger (endonym — the language's own name: "Français", not "French").
  • Mark the active language with both a check icon and aria-current="true" — never color alone.
  • Keep the language list short and stable; order consistently across sessions.
  • Place it in the header near the theme switcher and user menu.

Don't

  • Don't rely on a flag icon to denote a language — flags map to countries, not languages.
  • Don't reload or navigate abruptly on selection without preserving the user's place.
  • Language labels use the endonym in that language's own script: English, Français, Español, Bahasa Indonesia.
  • Trigger aria-label: "Language: [current language]".
  • Menu aria-label: "Select language".
  • Do not translate the endonyms — they are always shown in their own language.

Keyboard

Key Behavior
Tab Move focus to the language-switcher trigger
Enter / Space Open the menu; focus moves to the active (or first) language
Up / Down Arrow Move between languages in the open menu
Home / End Jump to the first / last language
Enter Select the focused language and close the menu
Escape Close the menu without changing the language; focus returns to the trigger

Accessibility

  • Trigger: aria-haspopup="menu" and aria-expanded reflecting open state; aria-label="Language: [current language]".
  • Menu semantics: role="menu" on the panel, role="menuitemradio" on each language (single-choice list), aria-label="Select language" on the menu.
  • Active language: the current language carries aria-current="true" (and aria-checked="true" on its menuitemradio) and a visible check icon — state is conveyed by icon + text, not color alone.
  • Language metadata: set lang on each item to its language tag (en, fr, es, id) so assistive tech pronounces each endonym correctly.
  • Focus management: on open, focus moves to the active language; on close, focus returns to the trigger.
  • Selection announcement: on change, announce the new language via a visually hidden aria-live="polite" region ("Language set to Français").

Spec

Values and token references resolved straight from the Zetta spec.

language-switcher-trigger
display
flex
alignItems
center
gap
{spacing.xs}
height
32px
paddingX
{spacing.sm}
borderRadius
{rounded.base}
backgroundColor
transparent
borderColor
transparent
borderWidth
1px
textColor
{colors.muted}
fontFamily
Geist
fontSize
14px
fontWeight
500
iconSize
{icons.sizeMap.md}
chevronSize
{icons.sizeMap.sm}
cursor
pointer
language-switcher-trigger-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
language-switcher-trigger-active
backgroundColor
{colors.surface-pressed}
language-switcher-trigger-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
language-switcher-menu
backgroundColor
{colors.popover}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
shadow
{elevation.shadow.overlay}
padding
{spacing.xs}
minWidth
180px
zIndex
{elevation.z-index.popup}
language-switcher-item
display
flex
alignItems
center
gap
{spacing.sm}
paddingX
{spacing.sm}
paddingY
{spacing.xs}
borderRadius
{rounded.sm}
fontFamily
Geist
fontSize
14px
fontWeight
400
lineHeight
1.40
textColor
{colors.ink}
iconColor
{colors.muted}
iconSize
{icons.sizeMap.md}
cursor
pointer
minHeight
32px
language-switcher-item-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
language-switcher-item-focus
outline
2px solid {colors.border-focus}
outlineOffset
-2px
language-switcher-item-active
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
iconColor
{colors.primary}
fontWeight
500