· v0.6.5 · MIT License · Source: language-switcher.md
Language switcher
The language switcher is a GET navigation form for changing the document language. Choosing a language and pressing the visible submit navigates to ?lang=<code>; the server answers with a 303 redirect to the localized URL. It is deliberately not a POST mutation and never auto-submits from script: the control works with zero JavaScript, exactly like every other Gelium UI pattern.
Examples
A labeled switcher with three locales, Spanish currently selected.
Without the label slot the control stands alone — keep an accessible name another way if you drop it (for example aria-label on the <select>).
Guidance
When to use
Use a language switcher when one document is served in several languages and the user must be able to move between them at any time — typically in the footer or the header chrome. It belongs wherever locale is a property of the whole page, not of one fragment.
When not to use
Do not use it to change display preferences such as theme or density — those are theme concerns with their own switchers. Do not use it as a general select-and-submit filter: a data filter belongs in Select inside its own form contract. And do not use it when the page ships in a single language — a one-option switcher is noise.
Usability
- Keep the submit visible. Auto-submitting on
changebreaks keyboard review, screen-reader expectations, and no-JS users alike. - Name locales in their own language (
Español, notSpanish) so speakers can find theirs without reading the current language first. - Mark the current locale with
selected; the server renders it, the user never guesses where they are. - Answer the GET with
303 See Otherso the localized URL becomes the address bar URL and refresh/reload stays correct.
Accessibility
- The native
<select>carries the semantics; pair it with a visible<label>, or give the control anaria-labelwhen the label slot is omitted. - The focus ring uses
--ui-focus-*tokens with:focus-visible, so keyboard focus is visible and mouse clicks stay clean. - In forced-colors mode the label paints
CanvasTextand the select keeps aCanvasTextborder, so the control survives palette removal.
See Choose the right control for the cross-component decision.
Anatomy
.ui-language-switcher— the root<form method="get">: a wrapping flex row with--ui-language-switcher-gap..ui-language-switcher-label— the optional visible label bound to the select byfor/id; body-sm type in muted foreground..ui-language-switcher-control— the row holding the select and the submit button together..ui-language-switcher-select— the native<select name="lang">: surface background, small radius, border token, and the shared focus-visible ring.- Submit — a standard Button (
secondary) rendered by thebuttontemplate; always visible.
Variants
- With label — the default pairing of visible label and control.
- Label-free — the control alone, named through
aria-labelon the select.
Anti-patterns
- Do not POST from the switcher: changing language is navigation, not mutation — a GET keeps it idempotent, cacheable, and bookmarkable.
- Do not hide the submit button or fire the form from
changeevents; the visible submit is the no-JS contract. - Do not hardcode
left/rightstyling: the flex row flows in document direction, so RTL documents mirror it for free.
Checklist
- The form is
method="get"withname="lang"on the select. - The current locale renders
selected; locales are written in their own language. - The submit button is visible and reachable by keyboard before the change takes effect.
- The server answers the GET with
303 See Otherto the localized URL. - A visible label or an
aria-labelnames the control.
Accessibility
The switcher is built entirely from native elements — a labelled <select> inside a real form — so assistive technology announces the role, value, and current locale without extra ARIA. Focus follows the natural tab order into the select and then the visible submit, both painted with the shared focus-visible ring, and forced-colors mode keeps label and boundary legible when the palette is removed.