LanguageSwitcher
A button showing the page's language that opens the others, each in its own name, as links to the same page in it.
Demo
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/language_switcher.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/language_switcher.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/language_switcher.qwe
let language_switcher_demo_langs = [ { code: "en", label: "English", href: "#language_switcher", flag: "🇬🇧" }, { code: "pt", label: "Português", href: "#pt", flag: "🇵🇹" }, { code: "es", label: "Español", href: "#es", flag: "🇪🇸" }, { code: "de", label: "Deutsch", href: "#de", flag: "🇩🇪" }, { code: "ja", label: "日本語", href: "#ja", flag: "🇯🇵" }]component LanguageSwitcherDemoLive() { state code = "fr" <div.demo-row> <LanguageSwitcher id="language-switcher-demo-live" current={code} short={true} variant="outline" languages={[ { code: "fr", label: "Français" }, { code: "it", label: "Italiano" }, { code: "nl", label: "Nederlands" } ]} on_select={(c) => { code = c }}/> <span.demo-note>Picked: {code}</span> </div>}view LanguageSwitcherDemo() = <div.demo-stack> <div.demo-row> <LanguageSwitcher id="language-switcher-demo" current="en" languages={language_switcher_demo_langs}/> <LanguageSwitcher id="language-switcher-demo-end" current="ja" align="end" variant="soft" size="md" languages={language_switcher_demo_langs}/> </div> <LanguageSwitcherDemoLive/> </div>Source
ui/language_switcher.qwe
// LanguageSwitcher: a button showing the language the page is in, that opens the list// of the others, each written in its own language, as links to the same page in it.// It is the browser's `popover`, so Escape or a click elsewhere closes it, and it needs// no script. Each link carries `lang` and `hreflang`, so browsers and screen readers// read the names right.//// <LanguageSwitcher id="lang" current="en" languages={[// { code: "en", label: "English", href: "/en/docs", flag: "🇬🇧" },// { code: "pt", label: "Português", href: "/pt/docs", flag: "🇵🇹" },// { code: "ja", label: "日本語", href: "/ja/docs", flag: "🇯🇵" }// ]}/>//// id (required): unique on the page; the button opens the list by it.// languages: { code (a BCP 47 tag), label (the name in that language), href, flag? }.// current: the code of the page's language; the button shows it, and it is marked in// the list (aria-current="true"). An item without an href is a button that, inside one// of your components, calls on_select(code). short: the button shows the code ("EN")// instead of the name. align: "start" (default) or "end" (the list's right edge under// the button's). variant and size as a Button ("ghost", "sm"). label: what screen// readers call the list ("Language").import ui/theme.qwe use ui_clslet language_switcher_css = css`.ui-lang-btn svg { width: 16px; height: 16px; flex: none; }.ui-lang-btn .ui-lang-chev { width: 14px; height: 14px; margin-right: -4px; transition: transform .15s; }.ui-lang-btn:has(+ :popover-open) .ui-lang-chev { transform: rotate(180deg); }.ui-lang-flag { font-size: 16px; line-height: 1; }.ui-lang { min-width: 180px; max-width: min(300px, calc(100vw - 16px)); margin: auto; padding: 5px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.4 var(--ui-font);}.ui-lang ul { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }.ui-lang-item { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; cursor: pointer;}.ui-lang-item:hover, .ui-lang-item:focus-visible { outline: none; background: var(--ui-subtle); }.ui-lang-item:focus-visible { box-shadow: var(--ui-focus); }.ui-lang-item[aria-current] { color: var(--ui-accent); font-weight: 500; }.ui-lang-item span:nth-child(2), .ui-lang-item > span:only-child { flex: 1; }.ui-lang-code { color: var(--ui-muted); font-size: 12px; text-transform: uppercase; }.ui-lang-item svg { width: 14px; height: 14px; flex: none; color: var(--ui-accent); }.ui-lang[popover]:popover-open { animation: ui-lang-in .12s ease-out; }@keyframes ui-lang-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) { .ui-lang { inset: auto; margin: 4px 0; position-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline; } .ui-lang-end { position-area: bottom span-left; }}@supports not (position-area: bottom) { .ui-lang::backdrop { background: var(--ui-overlay); } }@media (prefers-reduced-motion: reduce) { .ui-lang[popover]:popover-open { animation: none; } .ui-lang-btn .ui-lang-chev { transition: none; } }`view LanguageSwitcher(id: str, languages: list = [], current: str? = null, short: bool = false, align: str = "start", variant: str = "ghost", size: str = "sm", label: str = "Language", on_select = null, class: str? = null) { style global language_switcher_css let now = find(languages, (l) => l.code == current) ?? (len(languages) > 0 ? languages[0] : null) <span class={class}> <button type="button" popovertarget={id} aria-label={now != null ? `${label}: ${now.label}` : label} class={ui_cls(["ui-btn", `ui-btn-${variant}`, size != "md" && `ui-btn-${size}`, "ui-lang-btn"])} style={`anchor-name: --${id}`}> {now != null && get(now, "flag") != null ? <span.ui-lang-flag aria-hidden="true">{now.flag}</span> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>} <span lang={now != null ? now.code : null}>{now == null ? label : short ? upper(now.code) : now.label}</span> <svg.ui-lang-chev viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg> </button> <div id={id} popover="auto" class={ui_cls(["ui-lang", align == "end" && "ui-lang-end"])} style={`position-anchor: --${id}`}> <ul aria-label={label}> {for l in languages => { let here = now != null && l.code == now.code let body = [ get(l, "flag") != null && <span.ui-lang-flag aria-hidden="true">{l.flag}</span>, <span lang={l.code}>{l.label}</span>, <span.ui-lang-code aria-hidden="true">{l.code}</span>, here && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>, ] <li>{get(l, "href") != null ? <a.ui-lang-item href={l.href} hreflang={l.code} aria-current={here ? "true" : null}>{body}</a> : <button.ui-lang-item type="button" popovertarget={id} popovertargetaction="hide" aria-current={here ? "true" : null} @click={() => { on_select && on_select(l.code) }}>{body}</button>}</li> }} </ul> </div> </span>}Related
UserMenuAn avatar button that opens the account menu: name and email, links, and sign out; the arrow keys move through it.5 views · 6 downloadsCollapsibleNavSide navigation in groups that fold under their titles, the browser remembering which are open.6 views · 7 downloadsMegaMenuA top bar whose items open wide panels of grouped links under the whole bar, on hover and from the keyboard, with no script.5 views · 7 downloadsMenuA vertical list of links and actions in titled sections, with icons, shortcuts, counts, disabled items and the active one marked.6 views · 6 downloadsMobileNavA menu button that opens the site's navigation over the whole screen, as a dialog: groups, the current page, actions at the bottom.5 views · 6 downloadsSideNavAn app's navigation down the side: sections, groups that open, icons, counts; it folds to its icons.4 views · 6 downloads
Thread
Thread 0 posts
On LanguageSwitcher: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.