Language and Currency Switcher Dropdown with Alpine.js
AI generated
x-data
Alpine
Alpine.js · Dropdown · i18n · UX Pattern
Language and Currency Switcher Dropdown with Alpine.js
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.

12 min read x-data · @click.outside · keyboard navigation · localStorage Alpine.js 3.x · Magento store switcher

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.

11. FAQ: Language and Currency Switcher with Alpine.js

1How do I build the switcher with Alpine.js?
x-data dropdown with @click.outside, separate arrays for languages and currencies, real server action on selection.
2One dropdown or two?
Combined saves space, separate is better when language and currency are frequently chosen independently.
3How is the selection applied server side?
Form submit to a store switch endpoint that sets session values and redirects.
4How does keyboard navigation work?
ArrowDown/ArrowUp move focus, Enter selects, Escape closes and returns focus.
5Why are flag icons problematic?
A flag represents a country, not a language, and does not correctly represent multilingual regions.
6How do I persist the selection client side?
localStorage for immediate pre selection, server state remains the authoritative source.
7Where does the x-data object's data come from?
Injected as JSON from a view model, backend configuration remains the single source of truth.
8How to stay on the same page after switching?
Current URL is passed to the store switch endpoint, which computes the matching target URL.
9Which ARIA role does the dropdown need?
role=listbox for the container, role=option per entry, aria-selected for the active option.
10Does it need an extra library?
No, Alpine.js alone is enough for state, keyboard, and form based server communication.