Universally ships with a built-in dropdown switcher that you can drop in via auto-placement, the [universally_switcher] shortcode, or the universally_switcher() PHP function. That covers most cases.
But if you want complete control over how the switcher looks and behaves: a row of inline tabs, flags-only buttons, a vertical sidebar list, a custom dropdown that matches your design system, anything. You can build your own in PHP using the same data the plugin uses internally.
This guide shows you:
- The foundation: the one helper function you need, the data it returns, and a minimal “Hello World” switcher you can build on.
- A complete worked example: inline tabs (flags + native names) with alignment options, toggles, and styles, that you can use as-is or adapt.
The foundation
Universally exposes a single PHP helper that returns everything you need to render a switcher:
universally_get_switcher_urls();
It returns an array of language entries. Each entry contains:
| Field | Type | Description |
|---|---|---|
name |
string | English display name, with the region or variant in brackets (e.g. Spanish (Mexico), English (British)). |
originalName |
string | Native name only, no region (e.g. Deutsch, English, Français). |
flagUrl |
string | URL to the flag icon, or an empty string when no flag is mapped for the region. Always present, whatever the Country Flags setting says. |
urlPrefix |
string | URL prefix for the language (e.g. de, fr, mx). Empty for the source language. |
lang |
string | Bare language code (e.g. es, en). |
variant |
string | Translation variant, lower case (e.g. es-419, en-gb). Empty for the source language. |
region |
string | Full region code, lower case (e.g. es-mx, de-de). Not a bare country code. |
isSource |
bool | true for the source language. |
isDisabled |
bool | true when the language is added but disabled. Target languages only. |
url |
string | Full URL to the current page in this language. |
isCurrent |
bool | true for the language the visitor is currently viewing. |
Skip disabled languages. A disabled language still appears in this array, but its URL is no longer served as a translation, so a link to it is a dead end. The built-in switcher filters them out and your switcher should too: that is the isDisabled check in the examples below.
Use region when you need a locale value for a lang attribute or an hreflang, since it is the full code rather than a country abbreviation.
Once you have this array, the rest is up to you: what HTML you wrap it in, what CSS you apply, what behaviour you add. Universally doesn’t care.
Minimal foundation example
The smallest possible custom switcher: loop through the languages, output a link for each, expose it as a shortcode. No styles, no options, no markup beyond what’s required.
<?php
function custom_language_switcher() {
if (!function_exists('universally_get_switcher_urls')) return '';
$langs = universally_get_switcher_urls();
if (empty($langs)) return '';
$out = '<ul class="custom-language-switcher">';
foreach ($langs as $lang) {
if (empty($lang['url'])) continue;
if (!empty($lang['isDisabled'])) continue;
$label = esc_html($lang['originalName'] ?? '');
$url = esc_url($lang['url']);
$flag = !empty($lang['flagUrl'])
? '<img src="' . esc_url($lang['flagUrl']) . '" alt="">'
: '';
$out .= '<li><a href="' . $url . '">' . $flag . $label . '</a></li>';
}
$out .= '</ul>';
return $out;
}
add_shortcode('custom_language_switcher', 'custom_language_switcher');
Paste this into your child theme’s functions.php (or a code-snippets plugin like WPCode) and use it anywhere shortcodes are accepted:
[custom_language_switcher]
…or call it from PHP (e.g. in a theme template file):
<?php if (function_exists('custom_language_switcher')) echo custom_language_switcher(); ?>
That’s the foundation. From here you can:
- Add CSS to lay it out however you want (row of tabs, vertical menu, grid…).
- Add shortcode attributes to make it configurable.
- Highlight the current language using
$lang['isCurrent']. - Hide the source language, show only certain languages, group them, etc.
The worked example below shows what that can look like in practice.
Worked example: inline language tabs
A full-featured custom switcher built on top of the foundation above. It renders the languages as a horizontal row of tabs with flags and native names, useful for placing in a header, next to a search bar, or anywhere else you want a non-dropdown switcher.
It adds:
- Three alignment options:
left,right,center. - Toggles for flags and names independently (e.g. flags-only).
- Built-in CSS so it looks right without you writing any styles.
- Highlights the current language with
aria-current="page"and a--currentclass for styling. - Namespaced classes (
custom-uni-tabs*) so it won’t collide with anything else.
⚠️ Use this OR the foundation snippet above, not both at the same time, because they would both define a shortcode. (You can rename either one to use them side by side.)
Add the snippet
Paste this into your child theme’s functions.php (or WPCode):
<?php
function custom_uni_tabs($atts = []) {
if (!function_exists('universally_get_switcher_urls')) return '';
$langs = universally_get_switcher_urls();
if (empty($langs)) return '';
$atts = shortcode_atts([
'align' => 'center',
'show_flags' => 'true',
'show_names' => 'true',
], $atts);
$align = in_array($atts['align'], ['left', 'right', 'center'], true) ? $atts['align'] : 'center';
$showFlags = filter_var($atts['show_flags'], FILTER_VALIDATE_BOOLEAN);
$showNames = filter_var($atts['show_names'], FILTER_VALIDATE_BOOLEAN);
if (!$showFlags && !$showNames) {
$showNames = true; // never render an empty link
}
static $printed_styles = false;
$styles = '';
if (!$printed_styles) {
$printed_styles = true;
$styles = '<style>
.custom-uni-tabs { display: flex; align-items: center; gap: 16px; list-style: none; margin: 0; padding: 0; }
.custom-uni-tabs--left { justify-content: flex-start; }
.custom-uni-tabs--right { justify-content: flex-end; }
.custom-uni-tabs--center { justify-content: center; }
.custom-uni-tabs__item { margin: 0; padding: 0; }
.custom-uni-tabs__link { display: inline-block; margin: 0; padding: 0; text-decoration: none; line-height: 1; }
.custom-uni-tabs__link--current { font-weight: 600; }
.custom-uni-tabs__flag { width: 20px; height: auto; vertical-align: middle; }
.custom-uni-tabs__flag--with-label { margin-right: 6px; }
.custom-uni-tabs__label { vertical-align: middle; }
</style>';
}
$out = $styles . '<ul class="custom-uni-tabs custom-uni-tabs--' . esc_attr($align) . '">';
foreach ($langs as $lang) {
if (empty($lang['url'])) continue;
if (!empty($lang['isDisabled'])) continue;
$label = esc_html($lang['originalName'] ?? '');
$url = esc_url($lang['url']);
$isCur = !empty($lang['isCurrent']);
$flag = '';
if ($showFlags && !empty($lang['flagUrl'])) {
$flagCls = 'custom-uni-tabs__flag' . ($showNames ? ' custom-uni-tabs__flag--with-label' : '');
$alt = $showNames ? '' : $label;
$flag = '<img class="' . $flagCls . '" src="' . esc_url($lang['flagUrl']) . '" alt="' . esc_attr($alt) . '">';
}
$linkCls = 'custom-uni-tabs__link' . ($isCur ? ' custom-uni-tabs__link--current' : '');
$aria = $isCur ? ' aria-current="page"' : '';
$out .= '<li class="custom-uni-tabs__item">';
$out .= '<a class="' . $linkCls . '" href="' . $url . '"' . $aria . '>';
$out .= $flag;
if ($showNames) {
$out .= '<span class="custom-uni-tabs__label">' . $label . '</span>';
}
$out .= '</a></li>';
}
$out .= '</ul>';
return $out;
}
add_shortcode('custom_uni_tabs', 'custom_uni_tabs');
Use it
Shortcode
[custom_uni_tabs]
Attributes:
| Attribute | Values | Default | What it does |
|---|---|---|---|
align |
left, right, center |
center |
Horizontal alignment of the row. |
show_flags |
true, false |
true |
Show/hide flag icons. |
show_names |
true, false |
true |
Show/hide native language names. |
Examples:
[custom_uni_tabs]
[custom_uni_tabs align="right"]
[custom_uni_tabs show_names="false"] (flags only)
[custom_uni_tabs show_flags="false" align="left"] (names only, left)
PHP
For when you want to drop it directly into a theme template file (e.g. header.php), into a hook, or into a page-builder PHP/code widget. Call the function directly and pass any attributes as an array:
<?php
if (function_exists('custom_uni_tabs')) {
echo custom_uni_tabs([
'align' => 'center',
'show_flags' => 'true',
'show_names' => 'false',
]);
}
?>
⚠️ Edit
header.php(or any core theme file) only inside a child theme. Editing the parent theme directly means your changes will be lost the next time the theme updates.
Optional CSS overrides
The example ships with sensible defaults. Override them from your theme stylesheet whenever you need:
/* Bigger flags, more spacing, and a colour for the active language */
.custom-uni-tabs { gap: 24px; }
.custom-uni-tabs__flag { width: 24px; }
.custom-uni-tabs__link { font-size: 14px; color: #333; }
.custom-uni-tabs__link:hover { color: #000; }
.custom-uni-tabs__link--current { color: #c00; font-weight: 700; }
Class reference:
| Class | Applied to |
|---|---|
.custom-uni-tabs |
The outer <ul>. |
.custom-uni-tabs--left/right/center |
Alignment modifier on the <ul>. |
.custom-uni-tabs__item |
Each <li>. |
.custom-uni-tabs__link |
Each <a> (inline-block, no padding). |
.custom-uni-tabs__link--current |
The active language’s link. |
.custom-uni-tabs__flag |
The flag <img>. |
.custom-uni-tabs__flag--with-label |
Flag when shown alongside a name. |
.custom-uni-tabs__label |
The native-name <span>. |
Where to place the switcher
Once your shortcode is registered, you can place it anywhere in your site. The exact steps depend on your theme/builder:
- Block themes (Twenty Twenty-Four, etc.): open Appearance → Editor, find the template or template part you want to edit (e.g. Header, Footer, a single page), insert a Shortcode block, and paste
[custom_uni_tabs]. - Page builders (Elementor / Divi / Beaver Builder / Bricks): add a Shortcode or Code widget wherever you want the switcher and paste
[custom_uni_tabs]. - Classic themes: paste
[custom_uni_tabs]directly into a post, page, or widget area. To place it inside a theme template file (e.g.header.php,footer.php,sidebar.php), edit the file in a child theme and add:
<?php if (function_exists('custom_uni_tabs')) echo custom_uni_tabs(); ?>
Troubleshooting
Nothing appears. Make sure the Universally plugin is active and at least one target language is enabled in your project. The snippets return nothing when universally_get_switcher_urls() is unavailable or empty.
The list is empty or out of date. The language list is cached on your site for 15 minutes. Adding or removing a language normally refreshes it straight away, but that push cannot land on a site Universally cannot reach: a local or staging install, a firewalled host, a project with no domain set, or a request that timed out. A site that was only just connected can also still be holding an empty list. Opening Universally » General » Languages forces a refresh. Saving the plugin settings does not.
No flags show. The Country Flags setting does not affect a custom switcher: it only applies to the built-in one. Check whether flagUrl is empty for that language instead, which happens when no flag is mapped for its region. The setting is at Universally » Language Switcher » Country Flags if you also want to change the built-in switcher.
Visitors cannot get back to the source language. Viewing a translated URL stores the visitor's language for 30 days, and unprefixed URLs then redirect to it. The built-in switcher gets around that by adding ?universally_switch=source to the source-language link, which clears the stored choice. Reproduce that in your own switcher:
$url = $lang['url'];
if (!empty($lang['isSource'])) {
$url .= (strpos($url, '?') === false ? '?' : '&') . 'universally_switch=source';
}
The tabs aren’t aligning the way I expect. On the worked example, the align attribute positions items inside the <ul>, but the <ul> itself only takes the width of its parent container. If the parent isn’t full-width, the tabs may look off-centre or pushed to one side. Wrap the shortcode in a full-width container, or add:
.custom-uni-tabs { width: 100%; }
The default floating switcher is still showing. Go to Universally → Language Switcher and switch implementation from Auto to Custom, otherwise both will appear.
“Cannot redeclare function” error. You’ve pasted two snippets that define the same function name. Keep only one, or rename one of them.