jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Interactive

language-switcher — anchors, not buttons

Locales are links, not JavaScript state: every entry carries its own href, so the switcher works on fully prerendered sites with zero hydration owed. pair is the bilingual segmented group after the openspecui reference; menu is the dropdown for three or more locales — hard-shadow list on the terminal surface, closing on select, outside click, or Escape.

pair · menu SSG-safe anchors outside click · Escape close bezel-born styling

language-switcher

Locale switching in two variants, shown on the terminal bezel surface they were born on — open the menu: the list drops with a hard offset shadow and the current locale in brand hue.

driven by the playground
language-switcher-usage.svelte svelte
<LanguageSwitcher
  variant="pair"
  current="en"
  locales={[/* { code, label, href } */]}
/>

law

Why href, not onclick

A locale switch is navigation, not state mutation. Each locale knows the localized path of the current page, so the anchor carries the full destination and the switcher stays a pure link — crawlable, restorable, and functional before hydration (or without it entirely).

  • locales is data: { code, label, href } — pair renders the first two, menu renders all of them
  • menu semantics: a bare-disclosure BUTTON trigger (no aria-haspopup), a NAV landmark, and locale anchors carrying aria-current="page" — navigation, not listbox selection (the a11y section below teaches the same served truth)
  • styling speaks currentColor and the terminal tokens — it drops onto any bezel or themed surface unchanged
  • pair caps itself at two entries by design; the bilingual case is a distinct visual pattern, not a truncated menu

types

Types

Two variants split by locale count: pair for the bilingual case, menu for three or more.

language-switcher · types

pairsegmented group — caps at two entries by design
menunav disclosure — three or more locales
language-switcher-types-demo.svelte svelte
<script lang="ts">
  import LanguageSwitcher from '@ui/language-switcher.svelte';
</script>

<div class="flex flex-wrap items-start gap-6">
  <div class="flex flex-col gap-3 border border-border p-4"><span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">pair</span><LanguageSwitcher variant="pair" current="en" locales={[
      { code: 'en', label: 'EN', href: '#language-switcher-demo' },
      { code: 'zh', label: '中文', href: '#language-switcher-demo' },
    ]} /><span class="text-muted-foreground text-[12.5px]">segmented group — caps at two entries by design</span></div>
  <div class="flex flex-col gap-3 border border-border p-4"><span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">menu</span><LanguageSwitcher variant="menu" current="en" ariaLabel="Language" locales={[
      { code: 'en', label: 'English', href: '#language-switcher-demo' },
      { code: 'zh', label: '简体中文', href: '#language-switcher-demo' },
      { code: 'ja', label: '日本語', href: '#language-switcher-demo' },
      { code: 'de', label: 'Deutsch', href: '#language-switcher-demo' },
    ]} /><span class="text-muted-foreground text-[12.5px]">nav disclosure — three or more locales</span></div>
</div>

usage

Usage

locales is data — every entry carries the localized href of the current page.

LanguageSwitcher usage
<script lang="ts">
  import LanguageSwitcher from '@ui/language-switcher.svelte';
</script>

<!-- two locales: the segmented pair (active fills brand hue) -->
<LanguageSwitcher
  variant="pair"
  current="en"
  locales={[
    { code: 'en', label: 'EN', href: '/en/current-page' },
    { code: 'zh', label: '中文', href: '/zh/current-page' },
  ]}
/>

<!-- three or more: the dropdown menu (current in brand hue) -->
<LanguageSwitcher
  variant="menu"
  current="en"
  locales={[
    { code: 'en', label: 'English', href: '/en/current-page' },
    { code: 'zh', label: '简体中文', href: '/zh/current-page' },
    { code: 'ja', label: '日本語', href: '/ja/current-page' },
  ]}
/>

a11y

Accessibility

pair is a group of links; menu is a nav-landmark popover of real anchors — links navigate, so no listbox/option fiction (honesty pass, 2026-09-02).

Keyboard

KeyAction
TabMoves focus through the locale anchors (pair) or the trigger then the open list (menu)
EnterFollows the focused locale anchor — navigation, not state
EscapeCloses the menu; outside click closes it too

ARIA

AttributeValueDescription
aria-labelariaLabel ("Language")Accessible name for the menu trigger and pair group
aria-expandedtrue | falseOn the menu trigger — a bare disclosure, no haspopup (the panel is navigation, not a select).
rolegroup / navigationpair is a link group; the menu panel is a nav landmark (aria-label) of plain anchors — the current locale carries aria-current="page".

theming

Theming

Bezel-born: the switcher speaks currentColor and terminal surface tokens, so it inherits the surrounding chrome — no jx density tokens of its own.

TokenDefaultSource
currentColorinheritedcolor
terminal tokensbg-terminal / text-terminal-foregroundcolor

axes

Universal props

The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. Migrated contract: the structural pair/menu literal keeps its slot; density joins FRESH (the no-density-prop era ends with the axis surface).

LanguageSwitcher · universal props

Pure anchor navigation; the axes forward through the bezel.

language-switcher-universal.svelte svelte
<LanguageSwitcher size="medium">…</LanguageSwitcher>

api

API

Props from the LanguageSwitcher Props interface; SwitcherLocale is the data contract. Persistence: a locale click writes its code to localStorage key `lang` — the site's language bootstrap reads the same key.

Properties

PropertyTypeDefaultDescription
variant 'pair' | 'menu''pair' · Own default, not ambientSegmented bilingual pair or dropdown menu. Defaults: literal slot — own 'pair' (a structural selector, never a paint rung).
locales * readonly SwitcherLocale[]—{ code, label, href } entries; pair renders the first two, menu renders all.
current * string—Active locale code; matched against entry codes.
ariaLabel string'Language'Accessible name for the trigger / group.
storage key 'lang'target locale codeWritten on every locale click (try/catch, silent on storage failure); navigation stays pure anchors — the site's language-negotiation bootstrap reads this key.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h