---
title: "Language Switcher Guide - PerfLocale"
description: "Language switcher options - Gutenberg block, shortcode, widget, PHP template tag, and theme template overrides."
canonical: "https://perflocale.com/docs/language-switcher/"
source: "https://perflocale.com/docs/language-switcher/"
format: "markdown"
---
# Language Switcher

PerfLocale provides multiple ways to display a language switcher on your site.

## Gutenberg Block

Add the **Language Switcher** block from the block inserter (search "Language Switcher" or find it under Widgets).

### Block Settings

| Setting | Options | Default |
| --- | --- | --- |
| Style | Flags + Names, Flags Only, Names Only | Flags + Names |
| Layout | Horizontal, Vertical | Horizontal |
| Name Format | Native, English, Both - English (Native), Code | Native |
| Trigger Label Format (dropdown only) | Match Name Format, Native, English, Both, Code only — controls the dropdown button's visible label independently of the options' format. Use Code only to get a compact "EN" pill in the header while the dropdown lists full names. Hidden when Display Mode is not Dropdown. | Match Name Format |
| Show flags | On/Off | On |
| Show names | On/Off | On |
| Hide current | On/Off | On |
| Show untranslated | On/Off | Off |
| Dropdown arrow | Single chevron, Double chevrons, None (dropdown style only). Theme override via perflocale/switcher/arrow_html filter. | Single chevron |
| Font size | 10–24 px | 14 |
| Flag size | 14–40 px | 20 |
| Gap | 0–24 px | 8 |

The block uses server-side rendering, so you see a live preview in the editor.

The block also honors WordPress's standard block supports on the frontend - **Align** (left / center / right / wide / full), **Spacing** (padding / margin), **Color**, **Typography**, and **Additional CSS class** - so the same wrapper classes you see in the editor reach the page. Themes' `is-layout-constrained` layout rules cannot stretch the dropdown trigger to the full content-area width; the wrapper stays sized to the visible button.

## Shortcode: `[perflocale_switcher]`

Place this shortcode in any post, page, or widget area.

### Attributes

| Attribute | Values | Default | Description |
| --- | --- | --- | --- |
| style | flags_names, flags_only, names_only | From settings | Display style |
| display | inline, simple, dropdown | From settings | Layout mode. dropdown renders a click-to-expand button + listbox panel. |
| layout | horizontal, vertical | horizontal | List direction |
| name_format | native, english, both, slug | native | How to display language names in the options list |
| trigger_format | inherit, native, english, both, slug | inherit | Dropdown trigger button label format. inherit reuses name_format; an explicit value lets the header pill diverge from the options (e.g. slug trigger + native options renders "EN" on the button + "Deutsch / Français" inside the dropdown). Only meaningful when display="dropdown". |
| arrow_style | single, double, none | From settings | Dropdown trigger chevron icon. single = one down chevron (classic native-select look); double = stacked up + down chevrons; none = no icon (theme custom icons via the perflocale/switcher/arrow_html filter). Only meaningful when display="dropdown". |
| show_flags | 1 / 0 | 1 | Show flag emoji |
| show_names | 1 / 0 | 1 | Show language names |
| hide_current | 1 / 0 | From settings | Hide the active language |
| show_untranslated | 1 / 0 | From settings | Show languages without a translation |
| class | CSS class name | (empty) | Custom CSS class |

### Examples

```
[perflocale_switcher]

[perflocale_switcher display="dropdown"]

[perflocale_switcher style="flags_only" hide_current="1"]

[perflocale_switcher style="names_only" name_format="english"]

[perflocale_switcher layout="vertical" show_flags="0"]

[perflocale_switcher style="flags_names" name_format="both" class="my-switcher"]

[perflocale_switcher display="dropdown" arrow_style="double" trigger_format="slug"]
```

## Widget

Go to **Appearance > Widgets** and add the **Language Switcher** widget (in the PerfLocale section, or via the Legacy Widget block). The widget exposes the full set of per-instance options — Display Mode, Style, Layout, Name Format, Trigger Label Format, Dropdown Arrow, Show flags / names, Hide current, Show untranslated, Font / Flag / Gap sizes — matching the Gutenberg block one-for-one. The dropdown-only fields (Trigger Label Format, Dropdown Arrow) default to "Use site default" so existing widget instances keep the global Settings → Language Switcher value until you explicitly override.

## PHP Template Tag

In your theme templates:

```php
<?php perflocale_language_switcher(); ?>

<?php perflocale_language_switcher( [
	'template'       => 'flags_names',
	'display'        => 'dropdown',
	'show_flags'     => true,
	'show_names'     => true,
	'show_native'    => true,
	'hide_current'   => false,
	'name_format'    => 'native',
	'trigger_format' => 'slug',     // "EN" on the trigger button
	'arrow_style'    => 'double',   // up + down chevrons
	'class'          => 'my-custom-switcher',
] ); ?>
```

## Per-instance attribute coverage across surfaces

Every rendering surface — block, shortcode, template tag, WP widget, page-builder integrations, theme addons, the floating Customizer switcher — supports the same set of per-instance overrides. Unset / empty values fall through to the global **Settings → Language Switcher** defaults, so you can configure the site-wide look once and only override specific instances when needed.

| Surface | Trigger Label Format | Dropdown Arrow |
| --- | --- | --- |
| Gutenberg Block | Inspector → Display | Inspector → Appearance |
| Shortcode [perflocale_switcher] | trigger_format="…" | arrow_style="…" |
| PHP Template Tag perflocale_language_switcher() | 'trigger_format' => '…' | 'arrow_style' => '…' |
| WP Widget (Appearance → Widgets) | Trigger Label Format dropdown | Dropdown Arrow dropdown |
| Elementor widget | Trigger Label Format select (dropdown-only) | Dropdown Arrow select (dropdown-only) |
| Beaver Builder module | Trigger Label Format select | Dropdown Arrow select |
| Bricks element | Trigger Label Format select (dropdown-only) | Dropdown Arrow select (dropdown-only) |
| Customizer floating switcher | perflocale_floating_trigger_format control | perflocale_floating_arrow_style control |
| Blocksy header + footer language switcher | Trigger Label Format option (dropdown-only) | Dropdown Arrow option (dropdown-only) |
| Kadence header switcher | perflocale_kadence_trigger_format Customizer control | perflocale_kadence_arrow_style Customizer control |
| Neve header builder component | Trigger Label Format component setting | Dropdown Arrow component setting |

The dropdown-only controls (Trigger Label Format + Dropdown Arrow) are hidden in the UI when the surface's Display Mode is set to `inline` / `simple` — there's no trigger button to format in those modes. If you set them anyway (e.g. via shortcode attribute), the block render silently ignores them outside dropdown mode, so it's safe to script.

## Customising the switcher output

The switcher HTML is generated in PHP (`src/Frontend/LanguageSwitcher.php` and `LanguageSwitcherBlock.php`) - there are no PHP templates to copy into your theme. Customise it via filters and CSS instead:

```
// 1. Filter the language list before rendering (e.g. hide a language).
add_filter( 'perflocale/switcher/languages', function( $languages ) {
	return array_filter( $languages, fn( $lang ) => $lang->slug !== 'de' );
} );

// 2. Filter the <a> attributes for each link (add data attributes, classes, etc.).
add_filter( 'perflocale/switcher/link_attrs', function( $attrs, $lang, $current_slug, $args ) {
	$attrs['data-track'] = 'lang-switch:' . $lang->slug;
	return $attrs;
}, 10, 4 );

// 3. Inject HTML at the start / end of the dropdown panel.
//    Useful for region groupings, search inputs, branding chrome, etc.
//    Returned HTML is sanitised through kses_switcher() — extend the
//    allowlist with perflocale/switcher/kses_allowed_html if needed.
add_filter( 'perflocale/switcher/panel_before', function( $html, $languages, $current_slug, $attrs ) {
	return '<div role="presentation" class="my-switcher-heading">Choose your language</div>';
}, 10, 4 );

// 4. Swap the dropdown chevron for a custom icon.
add_filter( 'perflocale/switcher/arrow_html', function( $html, $style ) {
	return '<i class="fa-solid fa-chevron-down" aria-hidden="true"></i>';
}, 10, 2 );

// 5. Filter the inner HTML of every option (flag + label).
//    Same callback fires from dropdown, inline, and simple paths.
add_filter( 'perflocale/switcher/option_content', function( $inner, $lang ) {
	// Wrap in <bdi> for safe mixed-direction rendering of e.g.
	// "العربية" inside an LTR option list.
	return '<bdi>' . $inner . '</bdi>';
}, 10, 2 );

// 6. Filter the entire rendered HTML as a last-resort override.
add_filter( 'perflocale/switcher/output', function( $html, $args ) {
	// Wrap the switcher in your own container, swap markup, etc.
	return '<div class="my-switcher-wrap">' . $html . '</div>';
}, 10, 2 );
```

Built-in CSS class hooks (target these in your theme stylesheet to restyle without touching markup):

-   `.perflocale-switcher-block` - root nav element on the block/shortcode switcher
-   `.perflocale-switcher-block--dropdown` / `.perflocale-switcher-block--horizontal` / `.perflocale-switcher-block--vertical` / `.perflocale-switcher-block--simple` - mode modifier (set from the block’s _Display_ attribute)
-   `.perflocale-switcher-block__item`, `.perflocale-switcher-block__item--current`, `.perflocale-switcher-block__label`, `.perflocale-switcher-block__flag` - per-language item internals (used by horizontal / vertical / simple modes)
-   `.perflocale-dd__anchor` - inline-block wrapper inside the dropdown `<nav>` that shrink-wraps to the trigger button. The panel anchors here (not the wider `<nav>`) so `min-width: 100%` on the panel correctly means "at least as wide as the trigger".
-   `.perflocale-dd__trigger`, `.perflocale-dd__panel`, `.perflocale-dd__option` - dropdown internals
-   `.perflocale-dd__flag`, `.perflocale-dd__label`, `.perflocale-dd__arrow` - dropdown content slots

### CSS custom properties

The dropdown surface exposes every colour, spacing, timing, and z-index value as a `--perflocale-dd-*` custom property with a hardcoded fallback. A theme recolours / resizes the switcher by overriding the relevant vars instead of restating rules; the matching selector specificity is `(0,2,0)`, so any `(0,2,0)`\-or-higher selector wins.

```
/* Dark-theme palette. */
.my-dark-theme .perflocale-switcher-block--dropdown {
	--perflocale-dd-bg:                #1f2937;
	--perflocale-dd-text:              #f3f4f6;
	--perflocale-dd-border:            #374151;
	--perflocale-dd-border-hover:      #4b5563;
	--perflocale-dd-panel-bg:          #111827;
	--perflocale-dd-panel-border:      #1f2937;
	--perflocale-dd-option-text:       #d1d5db;
	--perflocale-dd-option-hover-bg:   #1f2937;
	--perflocale-dd-option-hover-text: #f3f4f6;
	--perflocale-dd-active-bg:         #312e81;
	--perflocale-dd-active-text:       #e0e7ff;
}

/* Tighter pill + faster open. */
.compact-header .perflocale-switcher-block--dropdown {
	--perflocale-dd-trigger-padding-block:  4px;
	--perflocale-dd-trigger-padding-inline: 8px;
	--perflocale-dd-trigger-radius:         12px;
	--perflocale-dd-transition-duration:    0.08s;
}
```

| Group | Variables |
| --- | --- |
| Surface colours | --perflocale-dd-bg, --perflocale-dd-text, --perflocale-dd-border, --perflocale-dd-border-hover, --perflocale-dd-focus-border, --perflocale-dd-focus-ring, --perflocale-dd-panel-bg, --perflocale-dd-panel-border, --perflocale-dd-panel-shadow |
| Option states | --perflocale-dd-option-text, --perflocale-dd-option-hover-bg, --perflocale-dd-option-hover-text, --perflocale-dd-active-bg, --perflocale-dd-active-text, --perflocale-dd-active-hover-bg |
| Trigger spacing | --perflocale-dd-trigger-padding-block, --perflocale-dd-trigger-padding-inline, --perflocale-dd-trigger-gap, --perflocale-dd-trigger-radius, --perflocale-dd-trigger-border-width, --perflocale-dd-trigger-focus-ring |
| Panel spacing | --perflocale-dd-panel-offset (gap below trigger), --perflocale-dd-panel-radius, --perflocale-dd-panel-border-width, --perflocale-dd-panel-padding-block, --perflocale-dd-panel-padding-inline, --perflocale-dd-panel-z-index |
| Option spacing | --perflocale-dd-option-padding-block, --perflocale-dd-option-padding-inline, --perflocale-dd-option-gap |
| Arrow / timing | --perflocale-dd-arrow-size (em), --perflocale-dd-arrow-opacity, --perflocale-dd-transition-duration, --perflocale-dd-option-transition-duration |
| Per-instance (set inline by the block / shortcode) | --perflocale-font-size, --perflocale-flag-size, --perflocale-gap |

All transitions on `.perflocale-dd__trigger`, `.perflocale-dd__arrow`, and `.perflocale-dd__option` are disabled inside a `@media (prefers-reduced-motion: reduce)` block, so users who opt out at the OS level don't see the cosmetic fades / arrow rotation.

### RTL and logical properties

The dropdown CSS uses logical properties throughout (`inset-inline-start` instead of `left`, `padding-block` / `padding-inline` instead of `padding: 7px 12px`, etc.). The panel anchors to the trigger's inline-start edge on LTR pages and inline-end on RTL pages without a separate RTL stylesheet, and the horizontal overflow flip in `language-switcher-dropdown.js` reads `getComputedStyle(panel).direction` so it measures viewport space against the right physical edge regardless of writing direction.

### Accessibility (lang, dir, ARIA)

Every option link, the dropdown trigger button, the disabled-option span, and the current-language span carry `lang="<target-slug>"`, plus `dir="rtl"` when the target language's `text_direction` is RTL. The visible text inside each option is in the TARGET language (_Français_, _العربية_), not the page language, so screen readers need `lang` to pronounce labels with the right phonemes, and the browser needs `dir="rtl"` to render RTL labels right-to-left even on an LTR page. The `perflocale/switcher/link_attrs` filter still lets you override these per-link if needed.

The dropdown trigger uses `aria-haspopup="listbox"`, `aria-expanded`, and `aria-controls`. The panel uses `role="listbox"` + `aria-labelledby` pointing at the trigger. Each option carries `role="option"` + `aria-selected="true"` on the current language; untranslated options that can't be activated carry `aria-disabled="true"` and are skipped by the JS arrow-key navigation. Keyboard: Enter / Space / ArrowDown opens + focuses option 1, ArrowUp opens + focuses the last option, arrows / Home / End move focus inside the panel (wrapping), a single printable character does first-letter type-ahead (mirrors native `<select>`), and Esc closes the panel and returns focus to the trigger.

### Live repositioning

While the panel is open, `scroll` (capture, passive), `resize`, and a `ResizeObserver` on the trigger button re-run the panel's positioning so it doesn't drift off-screen when the page reflows (sticky-header scrolls, viewport rotation, mobile browser chrome show/hide, web-font swap). The vertical anchor flips above if there's not enough room below (accounting for the WP admin bar); the horizontal anchor flips to inline-end if the inline-start anchor would push the panel past the viewport edge. Listeners attach on open and detach on close.

### JavaScript events

The dropdown switcher emits three namespaced `CustomEvent`s on its `<nav>` root so external JavaScript (analytics frameworks, A/B testing tools, custom theme triggers, headless integrations) can observe and influence switcher activity without forking the renderer. All three events bubble, so listeners can attach to the `<nav>` directly or to any ancestor (including `document`) and observe every switcher on the page from one place.

| Event | When it fires | Cancelable |
| --- | --- | --- |
| perflocale:switcher:open | After the panel opens (after the open class + aria-expanded="true" are applied and positioning has run). | No — the state change has already happened. |
| perflocale:switcher:close | After the panel closes — from clicking the trigger again, clicking outside, pressing Escape, OR being auto-closed because another switcher opened. One callback covers every closure path. | No. |
| perflocale:switcher:navigate | BEFORE the browser follows a clicked option's URL (also fires on keyboard activation — Enter on a focused option dispatches the same DOM click). | Yes — calling event.preventDefault() keeps the user on the current page. |

#### Event detail

```
// `perflocale:switcher:open` and `:close`
event.detail = {
	nav:     HTMLElement, // the switcher's <nav> root
	trigger: HTMLElement, // the <button> trigger
	panel:   HTMLElement, // the listbox <div>
}

// `perflocale:switcher:navigate`
event.detail = {
	nav:     HTMLElement, // the switcher's <nav> root
	trigger: HTMLElement, // the <button> trigger
	option:  HTMLElement, // the clicked <a> option
	slug:    string,      // language slug, from hreflang / lang attribute
	url:     string,      // option's href
}
```

#### Usage examples

```
// 1. GA4: send a `language_switch` event before the page navigates.
//    `sendBeacon` flushes the request even though the page is unloading.
document.addEventListener( 'perflocale:switcher:navigate', function ( e ) {
	navigator.sendBeacon(
		'https://www.google-analytics.com/g/collect',
		new Blob( [ JSON.stringify( {
			event:  'language_switch',
			from:   document.documentElement.lang,
			to:     e.detail.slug,
			source: 'header_pill'
		} ) ], { type: 'application/json' } )
	);
} );

// 2. Unsaved-changes guard: block language change if the user has
//    edited a form. preventDefault() keeps them on the current page.
document.addEventListener( 'perflocale:switcher:navigate', function ( e ) {
	if ( window.myAppHasUnsavedChanges ) {
		if ( ! confirm( 'You have unsaved changes. Switch language anyway?' ) ) {
			e.preventDefault();
		}
	}
} );

// 3. Open / close hooks on a specific switcher only — useful for
//    coordinating with a sticky header or pausing a hero-image
//    carousel while the dropdown is showing.
document.querySelector( '#header .perflocale-switcher-block--dropdown' )
	.addEventListener( 'perflocale:switcher:open', function ( e ) {
		document.body.classList.add( 'lang-switcher-open' );
	} );

document.querySelector( '#header .perflocale-switcher-block--dropdown' )
	.addEventListener( 'perflocale:switcher:close', function ( e ) {
		document.body.classList.remove( 'lang-switcher-open' );
	} );

// 4. Hotjar / Mixpanel: count every dropdown OPEN as an engagement
//    signal (separate from actual language switches).
document.addEventListener( 'perflocale:switcher:open', function ( e ) {
	if ( window.hj ) { hj( 'event', 'language_switcher_opened' ); }
} );
```

#### Notes on scope and behaviour

-   **Dropdown mode only.** The JS that fires these events is enqueued only when the switcher renders in `dropdown` display mode. In `inline` / `simple` modes the switcher is just a list of `<a>` links — attach a normal `click` listener to those links if you need to observe activations.
-   **Bubbling.** All three events bubble. Attach at the `<nav>`, at `document`, or anywhere in between. The event target is always the `<nav>`; `detail.nav` is included for ergonomic destructuring on delegated handlers.
-   **Cancelable navigate.** Only `perflocale:switcher:navigate` is cancelable. `open` and `close` fire AFTER the state change, so canceling them would leave the DOM inconsistent with the event.
-   **Browsers.** The script uses the `CustomEvent` constructor; if the page is somehow rendered in a context without it (IE, very old WebViews), the events are silently skipped rather than throwing. Every browser PerfLocale targets supports it.
-   **SDK precursor.** If a programmatic SDK is added later, these events remain valid public API — they're the read side of the listen / control split a future SDK would expose. Code written against them today won't break when the SDK lands.

### Language object properties (passed into filter callbacks)

| Property | Example | Description |
| --- | --- | --- |
| slug | fr | 2-3 letter language code |
| locale | fr_FR | Full WP locale |
| name | French | English name |
| native_name | Français | Native name |
| flag | fr | 2-letter flag country code (convert with PerfLocale\Helper::get_flag_emoji( $lang )) |
| text_direction | ltr | Text direction (ltr or rtl) |

## All switcher filters

The switcher exposes eight filter hooks. See the [Hooks reference](https://perflocale.com/docs/hooks/#language-switcher) for full parameter signatures.

| Hook | Purpose |
| --- | --- |
| perflocale/switcher/languages | Filter the list of languages the switcher renders. Applied on every render (not just first fetch), so context-aware shortcode wrappers can scope the list. |
| perflocale/switcher/link_attrs | Filter the HTML attributes for each per-language link / span. Add analytics data-*, override hreflang / lang / dir, inject rel / title, add CSS classes. |
| perflocale/switcher/option_content | Filter the inner HTML of each option (flag <span> + label <span> by default). Fires from every render surface — dropdown, inline, simple, list. Use to wrap labels in <bdi>, append region badges, prepend custom icons, etc. |
| perflocale/switcher/panel_before | Inject HTML at the START of the dropdown panel, inside the listbox, before the first option. Non-interactive chrome should be wrapped in <div role="presentation"> so the parent listbox doesn't announce it as an option. |
| perflocale/switcher/panel_after | Inject HTML at the END of the dropdown panel, inside the listbox, after the last option. Same role guidance as panel_before. |
| perflocale/switcher/arrow_html | Override the dropdown trigger's chevron icon. Built-in styles: single (down chevron), double (stacked up/down), none (empty). Returning a non-string falls back to empty. |
| perflocale/switcher/kses_allowed_html | Extend the wp_kses allowlist used by kses_switcher(). Needed when a custom arrow_html / panel_before / panel_after / option_content / link_attrs emits tags or attributes outside the built-in switcher set. |
| perflocale/switcher/output | Last-resort hook on the fully-rendered HTML. Wrap, prepend, or substitute. Runs after sanitisation. |

Every filter return that produces HTML (`arrow_html`, `panel_before`, `panel_after`, `option_content`) is automatically passed through `kses_switcher()` at the injection point — including on the Gutenberg block render path, which WordPress's `do_blocks()` doesn't otherwise sanitize. The renderer's per-attribute `esc_url()` / `esc_attr()` / `esc_html()` calls remain in effect on the built-in HTML it emits.

### Appending to a classic menu (no code)

On classic (non-block) themes, **Settings → Language Switcher → Append to classic menus** lists every registered classic menu location as a checkbox. Tick one and the language switcher is appended to the end of that menu automatically — no template edits, no filters. Mixed-case location slugs (e.g. `headerMenu`) are matched verbatim, so the append lands on the location the theme actually registered. Block themes should use the Language Switcher block instead.

Two additional hooks drive nav-menu auto-injection:

```
// Auto-inject the switcher items into a registered theme menu location.
// Return true to enable; pair with /switcher/menu_locations to pick which menu.
add_filter( 'perflocale/switcher/add_to_menu', '__return_true' );

// Restrict the auto-injection to specific theme menu locations.
add_filter( 'perflocale/switcher/menu_locations', function( $locations, $args ) {
	return [ 'primary', 'mobile' ];
}, 10, 2 );
```
