accessible, persistent, no framework needed
A language and currency switcher is one of the most frequently used, yet often poorly built UI elements in every international store. With Alpine.js you can build a dropdown that works with the keyboard, remembers its selection across sessions, and offers language and currency either cleanly separated or combined.
Table of Contents
- 1. Why a combined language and currency switcher is hard
- 2. Base structure: dropdown with x-data and click outside to close
- 3. Data model: languages and currencies as a structured array
- 4. Applying the selection: URL rewriting and store switching
- 5. Keyboard navigation: arrow keys, enter, and escape
- 6. Persisting the selection with localStorage and server sync
- 7. Combining flag icons and accessibility correctly
- 8. Combined dropdown vs. separate switchers
- 9. Switcher patterns compared
- 10. Summary
- 11. FAQ
1. Why a combined language and currency switcher is hard
A language and currency switcher looks at first glance like a simple dropdown, but quickly turns out in practice to be one of the more complex UI elements of an international store. Language and currency are two distinct dimensions that often, but not always, change together: a customer in Switzerland may want German text but prices shown in Swiss francs instead of euros. A switcher that forcibly couples both dimensions unnecessarily restricts exactly these legitimate combinations.
On top of that comes technical complexity: the selection must not only be visible client side in the UI, it also needs to be applied server side in most Magento stores, through a store view switch or a session variable. A purely client rendered language and currency switcher that fails to correctly forward the selection to the server causes inconsistencies between the visible UI and the content actually delivered.
The following sections build a complete language and currency switcher with Alpine.js: from the dropdown base structure, through the data model, actually applying the selection, keyboard support, persistence, and finally the question of whether language and currency should be offered in one dropdown or two separate ones.
2. Base structure: dropdown with x-data and click outside to close
The base of every language and currency switcher dropdown is a simple x-data object with a boolean for the open state. Clicking the trigger button opens the dropdown, clicking outside the dropdown closes it again. Alpine provides the built in @click.outside modifier for this, which needs no extra event listener code and automatically reacts only to clicks outside the element it is registered on.
A common mistake: developers forget that clicking an element inside the dropdown itself, for example a language option, should also close the dropdown once the selection has been applied. This behavior is not handled by @click.outside, it must be explicitly added inside the selection function itself with open = false.
<div x-data="switcherDropdown" class="relative" @click.outside="open = false">
<button
@click="open = !open"
class="flex items-center gap-2 px-3 py-2 rounded-lg border border-slate-200 text-sm"
:aria-expanded="open"
aria-haspopup="listbox"
>
<span x-text="currentLanguage.label + ' · ' + currentCurrency.code"></span>
<svg class="w-4 h-4" :class="{ 'rotate-180': open }"><!-- chevron --></svg>
</button>
<div
x-show="open"
x-transition
class="absolute right-0 mt-2 w-64 bg-white border border-slate-200 rounded-xl shadow-lg z-20"
>
<!-- Language and currency options -->
</div>
</div>
3. Data model: languages and currencies as a structured array
A clean language and currency switcher clearly separates the data model from the presentation. Languages and currencies each live as their own array of objects, with a code, a label, and an optional icon or flag reference. This separation allows both lists to be maintained independently and makes the mapping between language and country explicit, instead of guessing it implicitly through order or naming convention.
In a Magento context, these arrays are typically not hardcoded in JavaScript, but injected as JSON into the x-data attribute from a view model, so the actually configured store views and currencies from the backend remain the single source of truth. The currently active entry of each list is picked up from the server session or a cookie on initialization, not from a hardcoded default value.
// Language and currency switcher data model
document.addEventListener('alpine:init', () => {
Alpine.data('switcherDropdown', () => ({
open: false,
languages: [
{ code: 'de', label: 'Deutsch', flag: 'de' },
{ code: 'en', label: 'English', flag: 'gb' },
{ code: 'fr', label: 'Français', flag: 'fr' }
],
currencies: [
{ code: 'EUR', label: 'Euro', symbol: '€' },
{ code: 'CHF', label: 'Swiss Franc', symbol: 'CHF' },
{ code: 'USD', label: 'US Dollar', symbol: '$' }
],
currentLanguage: null,
currentCurrency: null,
init() {
// Read the currently active store from a data attribute injected by PHP
const activeLangCode = document.documentElement.lang || 'en';
const activeCurrencyCode = document.body.dataset.currentCurrency || 'EUR';
this.currentLanguage = this.languages.find(l => l.code === activeLangCode) || this.languages[0];
this.currentCurrency = this.currencies.find(c => c.code === activeCurrencyCode) || this.currencies[0];
}
}));
});
4. Applying the selection: URL rewriting and store switching
The decisive difference from a purely cosmetic dropdown: selecting a language and currency switcher entry must actually trigger a server action, not just a client side state change. In Magento, this happens through a store view switch sent as a GET parameter or POST request to a controller, which sets the session currency and then redirects to the matching store view URL.
Alpine's role here is merely to intercept the click, close the dropdown, and trigger the actual navigation, either through a direct form submit or through window.location.href with the target URL provided by the server. It matters that the URL for a language change accounts for the current page, switching on a product page should land back on that same product page after the language change, not on the homepage.
// Applying the language/currency selection via a store switch request
selectLanguage(language) {
this.currentLanguage = language;
this.open = false;
// The switch-language endpoint returns the equivalent URL on the target store
const form = document.createElement('form');
form.method = 'POST';
form.action = '/stores/store/switch';
form.innerHTML = `
<input type="hidden" name="store" value="${language.code}">
<input type="hidden" name="___current_url" value="${window.location.href}">
`;
document.body.appendChild(form);
form.submit();
}
5. Keyboard navigation: arrow keys, enter, and escape
A language and currency switcher operable only with the mouse systematically excludes keyboard users. A complete keyboard model needs at least four interactions: ArrowDown and ArrowUp move focus between options, Enter applies the currently focused option, and Escape closes the dropdown and returns focus to the trigger button.
Alpine allows this keyboard logic directly through key modifiers such as @keydown.arrow-down.prevent and @keydown.escape, with no manual keyCode comparison required. The focused index is held as its own state variable in the x-data object and incremented or decremented within the array bounds on every arrow key press, so navigation never runs off the end of the list.
<div
x-show="open"
role="listbox"
@keydown.arrow-down.prevent="focusedIndex = Math.min(focusedIndex + 1, languages.length - 1)"
@keydown.arrow-up.prevent="focusedIndex = Math.max(focusedIndex - 1, 0)"
@keydown.enter.prevent="selectLanguage(languages[focusedIndex])"
@keydown.escape="open = false; $refs.trigger.focus()"
>
<template x-for="(lang, index) in languages" :key="lang.code">
<button
role="option"
:aria-selected="lang.code === currentLanguage.code"
:class="{ 'bg-teal-50': index === focusedIndex }"
@click="selectLanguage(lang)"
class="w-full text-left px-4 py-2 text-sm"
x-text="lang.label"
></button>
</template>
</div>
6. Persisting the selection with localStorage and server sync
Even though the actual language and currency selection lives server side in the session, an additional client side persistence layer with localStorage is worthwhile, to show the last chosen combination immediately without flicker when a new session first loads. The trick: the client side stored value only serves for immediate visual pre selection of the dropdown, the actual, authoritative source always remains the server state.
A language and currency switcher that fails to keep both sources in sync quickly produces contradictory states, for instance when a user changes the language through a direct link without using the dropdown. The client side cache should therefore be validated against the actual server state on every page load and overwritten on mismatch, instead of blindly trusting the localStorage value.
7. Combining flag icons and accessibility correctly
Flag icons are a popular but problematic visual device in the language and currency switcher context, because a flag represents a country, not a language. Spanish is spoken in more than twenty countries, a single flag cannot capture that diversity and quickly leads to confusion or unintended political statements in multilingual regions.
The pragmatic solution: flag icons may be used as an additional visual element, but must always be accompanied by a text label, never used as the sole identifier. For screen reader users, the flag icon should be marked aria-hidden="true" anyway, since it is purely decorative, while the actual aria-label of the dropdown entry should always contain the full language name in that language itself, so users who do not understand the current interface language can still identify the option.
8. Combined dropdown vs. separate switchers
The decision between a single combined language and currency switcher and two separate dropdowns depends heavily on the store's context. A combined dropdown saves UI space and works well when language and currency in practice usually change together, for example with clearly separated country stores. Two separate switchers are preferable when users frequently choose language and currency independently of one another, as in the Switzerland example mentioned earlier.
Technically, the Alpine implementation barely differs: both variants share the same base pattern of x-data, keyboard navigation, and server synchronization, the difference lies only in whether one or two x-data components get instantiated, and whether the selection function sends one or two parameters to the server.
9. Switcher patterns compared
Different approaches to the language and currency switcher differ noticeably in UX and implementation effort.
| Pattern | UI space | Advantage | Drawback |
|---|---|---|---|
| Combined dropdown | One trigger, two sections | Saves header space | Often forces implicit coupling |
| Two separate dropdowns | Two triggers side by side | Full independence of selection | Needs more horizontal space |
| Full screen modal (mobile) | Entire screen as selection | Plenty of space for long lists | Interrupts reading flow more heavily |
| Footer only | No header space needed | Header stays uncluttered | Lower visibility and usage |
For most international stores, a combined dropdown in the header is recommended, with a full screen modal as the responsive variant on small screens, since coupling is rare and most users accept the suggested default combination of language and currency, as long as independent adjustment remains possible.
Mironsoft
Alpine.js components for multilingual Magento stores
A language and currency switcher that actually works?
We build accessible language and currency switchers with Alpine.js, including keyboard navigation, persistence, and correct integration with your Magento store view structure.
UX audit
Reviewing your existing switcher for accessibility and consistency
Built with Alpine.js
Combined or separate dropdown, matched to your store concept
Magento store switch
Clean integration with store views and session currency
10. Summary
A robust language and currency switcher built with Alpine.js needs more than a simple dropdown: it needs a cleanly separated data model for languages and currencies, an actual server action instead of pure UI cosmetics, complete keyboard navigation, and a deliberate decision between flag icons and text labels for accessibility. Persistence through localStorage improves perceived speed, but must never replace the authoritative server state.
The choice between combined and separate switchers is a product decision, not a purely technical one. Regardless of that choice, the Alpine.js base pattern stays identical: x-data for state, @click.outside to close, key modifiers for keyboard support, and a clear separation between the visual selection and the actual, server side application of language and currency.
Language and Currency Switcher with Alpine.js — The Essentials at a Glance
Data model
Languages and currencies as separate, structured arrays, injected from the backend.
Applying selection
Selection triggers a real store switch via form submit, not just client side cosmetics.
Keyboard
ArrowUp, ArrowDown, Enter, and Escape fully implemented, with role="listbox".
Accessibility
Flag icons always with a text label, never used as the sole identifier of a language.