Shopify · Guide · · Erick Nolasco
Turbo is a fast, good-looking theme, and lots of bigger Shopify stores still run it. It also has some keyboard problems built into its markup and CSS.
I spent time with five live stores running Turbo, using the Tab key instead of a mouse. Most of what broke came from the theme itself, so the fixes below should carry over to your store. Each one uses Turbo’s real class names, and I tested the JavaScript and CSS by injecting them into the live pages.
What I tested
Five stores running Turbo, on five versions: 3.1.2, 4.1.5, 6.1.2, 9.0.0 and 9.4.0. I checked product pages, the header, the mobile menu at phone width, and the cart link. I opened and closed menus and drawers by keyboard. I didn’t add anything to a cart or submit any form.
I only call something a Turbo problem if I saw it on at least two of these stores, or could trace it to Turbo’s own stylesheet or class names. Problems caused by apps or one store’s own changes are listed separately near the end.
1. Color and size swatches don’t work from the keyboard
What happens. Turbo builds each swatch from a real radio button followed by a styled div.swatch-element with a <label> inside. Then its stylesheet hides the radio with .swatch input { display: none; }. I found that exact rule in the stylesheets of all five stores. A radio with display: none can’t take focus, so the keyboard loses the one control that actually works.
How it fails depends on the version:
- Turbo 3.x and 4.x: the swatches never get focus. On the 3.1.2 and 4.1.5 stores, 150 Tab presses never landed on a swatch even once.
- Turbo 6.x and 9.x: Turbo adds
tabindex="0"to thediv.swatch-element, so Tab now reaches each swatch. But on the 6.1.2 and 9.4.0 stores, pressing Enter, Space or an arrow key on a swatch did nothing. The variant stayed the same. The 9.0.0 store has the same markup.
Who it affects. Anyone shopping by keyboard: people with motor disabilities, people who use switch devices, and screen reader users. If they can’t pick a size, they can’t buy.
The fix (works on every version I tested). Bring back the real radio buttons. Hide them visually, keep them focusable, and draw the focus ring on the swatch next to them. Add this to your theme CSS:
/* Keep Turbo's swatch radios focusable instead of display:none */
.swatch input[type="radio"] {
display: block !important;
position: absolute !important;
width: 1px !important;
height: 1px !important;
margin: -1px !important;
padding: 0 !important;
border: 0 !important;
overflow: hidden !important;
clip: rect(0 0 0 0) !important;
clip-path: inset(50%) !important;
white-space: nowrap !important;
}
/* The radio sits right before .swatch-element in Turbo's markup */
.swatch input[type="radio"]:focus-visible + .swatch-element {
outline: 2px solid #1a1a1a;
outline-offset: 2px;
}
On 6.x and 9.x, also delete tabindex="0" from the <div class="swatch-element"> in the swatch snippet. Search your theme code for swatch-element to find it. Leaving it in gives each swatch two Tab stops.
Before and after. Before, the 3.1.2 and 4.1.5 stores had zero swatch Tab stops. After, Tab landed on one radio per option group, and the arrow keys changed the variant and updated the ?variant= in the URL. That worked on 3.1.2, 4.1.5 and 9.4.0. On 6.1.2, Space selected the swatch and the URL updated. Radio buttons don’t react to Enter, and that’s normal behavior for them.
Quicker patch for 6.x and 9.x. If you’d rather leave the markup alone, this script makes Enter and Space click the swatch’s label:
document.addEventListener('keydown', function (e) {
var swatch = e.target.closest && e.target.closest('.swatch-element[tabindex]');
if (!swatch) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
var label = swatch.querySelector('label');
if (label) label.click();
}
});
When I injected this on the 6.1.2 and 9.4.0 stores, both Enter and Space selected the variant. Screen readers still won’t hear “radio button, 2 of 3” with this patch, so I’d treat the CSS fix as the real repair.
2. The mobile menu button can’t be reached
What happens. In Turbo 3.x through 6.x, the hamburger button is <a class="mobile_nav dropdown_link"> with no href. A link without an href can’t take focus. At phone width, the 3.1.2, 4.1.5 and 6.1.2 stores never focused the menu button in 40 Tab presses, and it wouldn’t take focus from a script either. The 9.0.0 store uses a <summary> element, which works fine.
Who it affects. Keyboard users on tablets, plus anyone who zooms a desktop browser far enough to get the mobile layout. At high zoom, the whole main navigation is behind this button.
The fix. In sections/header.liquid, find class="mobile_nav dropdown_link" and add role="button" tabindex="0" aria-expanded="false" to that <a>. Then add this script so Enter and Space open it and screen readers hear whether it’s open:
document.querySelectorAll('a.mobile_nav:not([href])').forEach(function (trigger) {
var menu = document.querySelector('[data-dropdown="' + trigger.dataset.dropdownRel + '"]');
trigger.setAttribute('role', 'button');
trigger.setAttribute('tabindex', '0');
trigger.setAttribute('aria-expanded', 'false');
trigger.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); trigger.click(); }
});
trigger.addEventListener('click', function () {
setTimeout(function () {
var open = menu && getComputedStyle(menu).display !== 'none';
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
}, 400);
});
});
Before and after. After injecting it, all three stores reached the button within the first three Tab presses. Enter opened Turbo’s .dropdown_container, aria-expanded changed to true, and the next Tab moved into the menu links.
3. A hidden sticky header steals the first Tab presses
What happens. On Turbo 6.x and earlier, the stylesheet hides the sticky header (.sticky_nav) by sliding it off screen with transform and fading it with opacity. Its links stay in the Tab order. On the 3.1.2 store, which has the sticky header turned on, the first 9 Tab presses went to invisible links before focus reached the real header. Turbo 9.x hides .sticky_nav with display: none, so newer versions seem to have fixed this.
Who it affects. Sighted keyboard users. They press Tab nine times and see nothing happen.
The fix:
.sticky_nav:not(.sticky_nav--stick) {
visibility: hidden !important;
}
Turbo adds .sticky_nav--stick when the header is meant to show, so this only hides it while it’s off screen. Once I added the rule, the invisible Tab stops were gone, and after scrolling down, the stuck header’s links still took focus.
4. Focus outlines are switched off
What happens. Turbo’s stylesheet removes focus rings in a few places. All five stores have .flickity-enabled:focus { outline: none; }, which affects the product image gallery. The 3.x, 4.x and 6.x stylesheets also set outline: 0 on .menu a, the header links. On the 3.1.2 and 6.1.2 stores, the visible header links showed no focus indicator at all. On the 6.1.2 store, an extra custom stylesheet also removed it.
Who it affects. Anyone using a keyboard who can see the screen. Without a ring, they can’t tell where they are.
The fix:
.main_nav a:focus-visible,
.main-nav a:focus-visible,
.menu a:focus-visible,
.top_bar a:focus-visible,
.top-bar a:focus-visible,
.flickity-enabled:focus-visible,
.swatch-element:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
Using :focus-visible means mouse users won’t see the ring when they click. When I injected it, the header links on the 3.1.2 store and the galleries on four stores went from no outline to a 2px solid one.
5. The product gallery has broken ARIA
What happens. Turbo uses the Flickity slider for product images. The .product_gallery container gets tabindex="0" but no role or name, so a screen reader lands on an unnamed box. Flickity also puts aria-selected on each .gallery-cell, which is a plain div. That attribute isn’t allowed on a div with no role, and axe reports it as critical. I counted 9, 14, 12 and 20 of these on the 4.1.5, 6.1.2, 9.0.0 and 9.4.0 stores. The arrow keys do move the slides, so the keyboard part works.
The fix. Give the gallery a name and strip the attribute, including after every slide change:
(function () {
function tidy(gallery) {
gallery.setAttribute('role', 'region');
gallery.setAttribute('aria-roledescription', 'carousel');
if (!gallery.hasAttribute('aria-label')) gallery.setAttribute('aria-label', 'Product images');
gallery.querySelectorAll('.gallery-cell[aria-selected]').forEach(function (cell) {
cell.removeAttribute('aria-selected');
});
}
document.querySelectorAll('.product_gallery, .product_gallery_nav').forEach(function (gallery) {
tidy(gallery);
new MutationObserver(function () { tidy(gallery); })
.observe(gallery, { subtree: true, attributes: true, attributeFilter: ['aria-selected'] });
});
})();
Before and after. On all four stores, the aria-allowed-attr count on gallery cells dropped to 0. It stayed at 0 after I moved the slider with the arrow keys.
Things that came from apps or store changes
These showed up on single stores and traced back to apps or custom code rather than Turbo, but they’re worth checking:
- Subscription widgets. A Skio frequency dropdown had
opacity: 0but still took focus, and it had no label. - Overlays. Two stores load an accessibility overlay widget. On one, it added two invisible Tab stops before anything else on the page. Neither overlay fixed the swatch or gallery problems above.
- Custom cart icons. One store replaced Turbo’s
a.mini_cart(which hashref="/cart") with an<a>that has nohref, so the cart couldn’t be reached by keyboard. - Search apps. A third-party search box collapsed to 0px wide but kept its Tab stop.
- Custom carousels. One store’s custom sections cloned slides with
aria-hidden="true", but 16 links inside those clones could still take focus.
Quick self-check
- Click the address bar and press Tab. Does the first stop land on something you can see?
- On a product page, can you get to a size or color with Tab and change it with Space or the arrow keys?
- Narrow the window or zoom to 300%. Can you open the menu with Tab and Enter?
- As you Tab through the header, can you always see where you are?
- Can you reach the cart from the keyboard?
How I tested
I used automated Chromium (Playwright) at 1280px wide, plus 390px for the mobile menu. I walked each page with real Tab key presses and recorded what had focus, whether you could see it, and whether it had a focus style. I pressed Enter, Space and the arrow keys on swatches, thumbnails and the gallery, and checked whether the variant ID changed. I ran axe-core against WCAG 2.2 AA rules and read the theme stylesheets to find where rules like display: none and outline: none came from. Fixes were injected into the live pages and then tested again the same way.
Limits: I didn’t test with a real screen reader, I didn’t test anything that needed a full cart, and five stores is a small sample. Each store has its own customizations, so treat your store as its own case.
WCAG 2.2 criteria involved
- 2.1.1 Keyboard (swatches, mobile menu)
- 2.4.3 Focus Order (hidden sticky header)
- 2.4.7 Focus Visible (removed outlines)
- 4.1.2 Name, Role, Value (gallery ARIA, swatch roles, unlabeled app controls)
- 1.4.10 Reflow (the mobile menu is how zoomed-in users get to navigation)
Running Turbo and want to know where your store stands? Get a free 5-point accessibility summary.
Want to know where your store stands?
I offer a free 5-point accessibility summary: the most important issues on your site, in plain language, with no obligation.
Get a free 5-point summary