Language switch

.lang-switch

Language switch — the same page in another language.

Examples

Two languages

View “Two languages” as
<div class="lang-switch">
  <button class="btn btn-sm lang-switch-toggle" type="button" id="lang-two"
          command="toggle-popover" commandfor="lang-two-menu">
    <svg class="icon" aria-hidden="true"><use href="#icon-languages"></use></svg>
    <span class="lang-switch-code">EN</span>
    <span class="sr-only">Language: English</span>
    <svg class="icon icon-sm" aria-hidden="true"><use href="#icon-chevron-down"></use></svg>
  </button>

  <div class="popover popover-align-end lang-switch-panel" id="lang-two-menu" popover data-ui="popover">
    <nav class="nav nav-vertical" aria-label="Language">
      <ul class="nav-list">
        <li><a class="nav-link lang-switch-option" lang="en" hreflang="en" href="#en" aria-current="true">English</a></li>
        <li><a class="nav-link lang-switch-option" lang="it" hreflang="it" href="#it">Italiano</a></li>
      </ul>
    </nav>
  </div>
</div>

Two links in a footer would have done, and that is exactly why this exists: there is no width at which a row of links becomes a list, so with a row the markup has to change somewhere between two languages and ten.

Here it does not. Each option is a real link because each is a different URL — middle-click, open in a new tab and hreflang all come free, and none of them survive a select that navigates on change.

Ten, in the same control

View “Ten, in the same control” as
<div class="lang-switch">
  <button class="btn btn-sm lang-switch-toggle" type="button" id="lang-ten"
          command="toggle-popover" commandfor="lang-ten-menu">
    <svg class="icon" aria-hidden="true"><use href="#icon-languages"></use></svg>
    <span class="lang-switch-code">EN</span>
    <span class="sr-only">Language: English</span>
    <svg class="icon icon-sm" aria-hidden="true"><use href="#icon-chevron-down"></use></svg>
  </button>

  <div class="popover popover-align-end lang-switch-panel" id="lang-ten-menu" popover data-ui="popover">
    <nav class="nav nav-vertical" aria-label="Language">
      <ul class="nav-list">
        <li><a class="nav-link lang-switch-option" lang="en" hreflang="en" href="#en" aria-current="true">English</a></li>
        <li><a class="nav-link lang-switch-option" lang="it" hreflang="it" href="#it">Italiano</a></li>
        <li><a class="nav-link lang-switch-option" lang="de" hreflang="de" href="#de">Deutsch</a></li>
        <li><a class="nav-link lang-switch-option" lang="fr" hreflang="fr" href="#fr">Français</a></li>
        <li><a class="nav-link lang-switch-option" lang="es" hreflang="es" href="#es">Español</a></li>
        <li><a class="nav-link lang-switch-option" lang="pt" hreflang="pt" href="#pt">Português</a></li>
        <li><a class="nav-link lang-switch-option" lang="nl" hreflang="nl" href="#nl">Nederlands</a></li>
        <li><a class="nav-link lang-switch-option" lang="pl" hreflang="pl" href="#pl">Polski</a></li>
        <li><a class="nav-link lang-switch-option" lang="el" hreflang="el" href="#el">Ελληνικά</a></li>
        <li><a class="nav-link lang-switch-option" lang="ja" hreflang="ja" href="#ja">日本語</a></li>
      </ul>
    </nav>
  </div>
</div>

The same markup, five times the languages, and the only difference is that the panel now scrolls: --lang-switch-max-block-size caps it at about seven rows so it cannot run off the bottom of a phone, and overscroll-behavior: contain stops the page moving underneath when you reach the end.

Every name is written in its own language and carries lang, which is what makes a screen reader pronounce "Français" in French instead of spelling it out in the page's voice.

Tokens 3

Level 2, declared on .lang-switch itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by lang-switch
TokenDefault
--lang-switch-code-font-sizevar(--font-size-xs)
--lang-switch-max-block-size17rem
--lang-switch-min-inline-size11rem

Variants and states

Variants

None. It is one shape, and the page does the rest.

Inside it

  • .lang-switch-code
  • .lang-switch-option
  • .lang-switch-panel
  • .lang-switch-toggle

State it reads

None.

Before you ship it

What you have to do 6 requirements

  1. Write each language's name in that language. "Deutsch", not "German". A reader who cannot read the current language cannot read the word for theirs in it, which is the entire failure this control exists to avoid.
  2. Put lang and hreflang on every option. lang makes a screen reader pronounce "Français" in French instead of spelling it in the page's voice; hreflang tells a crawler what is on the other end. They are two different statements and both are one attribute.
  3. Give the trigger a full name. The visible IT is an abbreviation and the icon is aria-hidden, so the button's accessible name comes from the .sr-only span: "Language: Italiano". Without it the button announces as "I T".
  4. Mark the current language with aria-current="true", and mark exactly one. It is the accessible signal as well as the visual one. page is wrong: the option is the same page, in another language.
  5. Add data-ui="popover" to the panel. Without it the trigger has no aria-expanded in any engine.
  6. Link the same page, not the home page. A switcher that drops the reader on the front page of the other language has lost their place, and that is the most common defect this control has.

src/css/components/lang-switch.css · npx mostlycss add lang-switch