Navigation & Accessibility

Free Language Switcher Widget

A compact dropdown that lets visitors flip between your site’s languages.

A lightweight inline dropdown that lists the languages your site is available in, each with an optional emoji flag, and links visitors to the translated version of the page. It highlights the current language and is perfect for multilingual blogs, stores, and business sites. Define your languages in one JSON attribute and install with a single script tag.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/language-switcher.js" data-hl-config='[{"label":"English","href":"/en/","flag":"🇺🇸"},{"label":"Español","href":"/es/","flag":"🇪🇸"},{"label":"Français","href":"/fr/","flag":"🇫🇷"},{"label":"Deutsch","href":"/de/","flag":"🇩🇪"}]' data-hl-current="English" async></script>
<!-- Free Language Switcher widget by haynes lab — https://hayneslab.dev/tools/widgets/language-switcher -->
Inline (self-contained)html
<!-- Free Language Switcher widget by haynes lab — https://hayneslab.dev/tools/widgets/language-switcher -->
<script data-hl-config='[{"label":"English","href":"/en/","flag":"🇺🇸"},{"label":"Español","href":"/es/","flag":"🇪🇸"},{"label":"Français","href":"/fr/","flag":"🇫🇷"},{"label":"Deutsch","href":"/de/","flag":"🇩🇪"}]' data-hl-current="English">
/*!
 * haynes lab — Language Switcher widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/language-switcher
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-language-switcher';
  var STYLE_ID = 'hlw-language-switcher-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/language-switcher.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; display: inline-block;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 14px; line-height: 1.4; color: #1f2937; text-align: left; }' +
      '.' + PREFIX + '__toggle { display: inline-flex; align-items: center; gap: 8px;' +
      ' padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #ffffff;' +
      ' color: #1f2937; font: inherit; cursor: pointer; }' +
      '.' + PREFIX + '__toggle:hover { border-color: #d1d5db; background: #f9fafb; }' +
      '.' + PREFIX + '__toggle:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__toggle[aria-expanded="true"] { border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__flag { font-size: 16px; line-height: 1; }' +
      '.' + PREFIX + '__chevron { font-size: 10px; color: #9ca3af; transition: transform 0.15s ease; }' +
      '.' + PREFIX + '__toggle[aria-expanded="true"] .' + PREFIX + '__chevron { transform: rotate(180deg); }' +
      '.' + PREFIX + '__menu { position: absolute; top: calc(100% + 6px); left: 0; z-index: 50;' +
      ' min-width: 100%; margin: 0; padding: 4px; list-style: none; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 8px;' +
      ' box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12); }' +
      '.' + PREFIX + '__menu[hidden] { display: none; }' +
      '.' + PREFIX + '__link { display: flex; align-items: center; gap: 8px; padding: 8px 10px;' +
      ' border-radius: 6px; color: #374151; text-decoration: none; white-space: nowrap; }' +
      '.' + PREFIX + '__link:hover { background: #f3f4f6; color: #111827; }' +
      '.' + PREFIX + '__link:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: -2px; }' +
      '.' + PREFIX + '__link[aria-current="true"] { font-weight: 600; color: var(--hlw-accent); }' +
      '.' + PREFIX + '__credit { display: block; margin-top: 4px; font-size: 11px;' +
      ' text-align: right; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: inline-block; vertical-align: -2px; margin-right: 4px; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function parseLanguages(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[haynes lab language-switcher] Missing required data-hl-config attribute (JSON array of {label, href, flag?}). Nothing rendered.');
      return null;
    }
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab language-switcher] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab language-switcher] data-hl-config must be a JSON array. Nothing rendered.');
      return null;
    }
    var valid = items.filter(function (l) {
      return l && typeof l.label === 'string' && l.label && typeof l.href === 'string' && l.href;
    });
    if (!valid.length) {
      console.warn('[haynes lab language-switcher] data-hl-config has no valid languages (each needs at least "label" and "href"). Nothing rendered.');
      return null;
    }
    return valid;
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function build(script) {
    var items = parseLanguages(script);
    if (!items) return;

    var current = script.getAttribute('data-hl-current') || '';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    var currentItem = null;
    if (current) {
      for (var i = 0; i < items.length; i++) {
        if (items[i].label === current || items[i].href === current) {
          currentItem = items[i];
          break;
        }
      }
    }

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);

    var toggle = el('button', PREFIX + '__toggle');
    toggle.type = 'button';
    toggle.setAttribute('aria-haspopup', 'true');
    toggle.setAttribute('aria-expanded', 'false');
    toggle.setAttribute('aria-label', 'Select language' + (currentItem ? ' (current: ' + currentItem.label + ')' : ''));
    if (currentItem && currentItem.flag) toggle.appendChild(el('span', PREFIX + '__flag', String(currentItem.flag)));
    toggle.appendChild(el('span', PREFIX + '__label', currentItem ? currentItem.label : (current || items[0].label)));
    toggle.appendChild(el('span', PREFIX + '__chevron', '▾'));

    var menu = el('ul', PREFIX + '__menu');
    menu.hidden = true;
    items.forEach(function (l) {
      var li = el('li');
      var link = el('a', PREFIX + '__link');
      link.href = l.href;
      if (l.flag) link.appendChild(el('span', PREFIX + '__flag', String(l.flag)));
      link.appendChild(el('span', null, l.label));
      if (l === currentItem) link.setAttribute('aria-current', 'true');
      link.addEventListener('click', close);
      li.appendChild(link);
      menu.appendChild(li);
    });

    function open() {
      menu.hidden = false;
      toggle.setAttribute('aria-expanded', 'true');
      document.addEventListener('click', onDocClick);
    }

    function close() {
      menu.hidden = true;
      toggle.setAttribute('aria-expanded', 'false');
      document.removeEventListener('click', onDocClick);
    }

    function onDocClick(e) {
      if (!root.contains(e.target)) close();
    }

    toggle.addEventListener('click', function () {
      if (menu.hidden) open(); else close();
    });

    root.addEventListener('keydown', function (e) {
      if (e.key === 'Escape' && !menu.hidden) {
        close();
        toggle.focus();
      }
    });

    root.appendChild(toggle);
    root.appendChild(menu);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/language-switcher';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab language-switcher] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-config(required)JSON array of languages, each with "label", "href", and an optional emoji "flag".
data-hl-current(none)The label (or href) of the active language — shown on the button and highlighted in the menu.
data-hl-accent"#f97316"Highlight color for the open state, focus ring, and current language.

Frequently asked questions

How do I install the Language Switcher?

Paste the script snippet where you want the switcher to appear — typically your header or footer — and it renders an inline dropdown right after the tag, listing the languages from your data-hl-config JSON.

How do I set the languages and the current one?

Define languages in data-hl-config as a JSON array of {"label","href","flag"?} entries (flag is an emoji), set data-hl-current to the active language’s label, and use data-hl-accent to match your brand color.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on WordPress, Shopify, Squarespace, or any site that can embed a script tag.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch