Hero ux en

Keyboard Navigation 2026: 6 BFSG Touchpoints Shops Miss

One year after Germany's BFSG (Barrierefreiheitsstärkungsgesetz) came into effect, the industry has made one clear patch priority: checkout. Form label associations, focus management between steps, error messages tied to inputs via aria-describedby - most DACH storefronts have that in reasonable shape now. What remains are six other touchpoints that get systematically overlooked.

That is not a footnote. Keyboard users hit the filter drawer, the mega-menu, and search autocomplete before they ever reach checkout. If those flows break down, the session ends early - not at the payment button.

This post walks through the six touchpoints in the order a keyboard user encounters them during a typical DACH storefront visit: from first navigation to cookie banner. Per touchpoint: the specific BFSG gap, the WCAG 2.2 criterion it falls under, and an actionable frontend pattern.

Touchpoint 1: Filter Drawer

The Gap

Filter drawer implementations share a recurring failure: when the drawer opens via keyboard, focus lands somewhere outside it - often on the last-focused element before the drawer triggered, sometimes on body. The filter options are technically reachable via Tab, but the user has to tab through the entire page first.

The second failure: when the drawer closes, focus does not return to the button that opened it. It goes nowhere. The user restarts from the top.

WCAG 2.2 SC 2.1.1 (Keyboard) and SC 2.4.3 (Focus Order) apply here. The BFSG mandates both criteria for storefronts.

The Pattern: Roving Tabindex in the Drawer

The anti-pattern is not the absence of a focus trap per se - it is the wrong focus containment concept. A modal dialog needs a hard focus trap. A non-modal filter drawer needs something different: a roving tabindex that restricts the focusable scope to the drawer while it is open, and returns focus precisely on close.

Concrete implementation:

// On drawer open
function openFilterDrawer(drawerEl, triggerEl) {
  drawerEl.setAttribute('aria-expanded', 'true');
  drawerEl.removeAttribute('inert');
  // Focus the first interactive element inside the drawer
  const firstFocusable = drawerEl.querySelector(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  firstFocusable?.focus();
  // Store trigger reference for return
  drawerEl._triggerEl = triggerEl;

// On drawer close function closeFilterDrawer(drawerEl) { drawerEl.setAttribute('aria-expanded', 'false'); drawerEl.setAttribute('inert', ''); // Return focus precisely to the trigger drawerEl._triggerEl?.focus(); } ```

The inert attribute is the key: it removes all elements inside a closed drawer from tab order and the accessibility tree without setting tabindex="-1" on each one individually. Browser support: all modern browsers from 2023 onward.

For the filter options inside the drawer, a roving tabindex pattern that enables arrow-key navigation within checkbox groups is a good addition - modelled on the ARIA Authoring Practices Guide Listbox pattern (https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).

Touchpoint 2: Mega-Menu

The Gap

Hover-only navigation is the oldest unsolved accessibility problem in storefront design. What matters for BFSG is not just whether the menu is reachable by keyboard - it is whether its hierarchy is communicated meaningfully to screen reader users.

The typical gap: the mega-menu is a nested <ul> structure with no ARIA semantics. Screen reader users do not know they are inside a navigation overlay. They do not know how deep the hierarchy goes. They do not know sub-menus exist before tabbing into them.

WCAG 2.2 SC 1.3.1 (Info and Relationships) and SC 4.1.2 (Name, Role, Value) apply.

The Pattern: Tree View with ARIA Semantics

The ARIA Authoring Practices Guide Tree View pattern (https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) is the reference implementation for hierarchical navigation structures.

Minimal implementation for a two-level mega-menu:

<nav aria-label="Main navigation">
  <ul role="tree">
    <li role="treeitem" aria-expanded="false" aria-haspopup="true"
        tabindex="0" id="nav-fashion">
      Fashion
      <ul role="group" aria-labelledby="nav-fashion" hidden>
        <li role="treeitem" tabindex="-1">
          <a href="/women">Women</a>
        </li>
        <li role="treeitem" tabindex="-1">
          <a href="/men">Men</a>
        </li>
      </ul>
    </li>
  </ul>
</nav>

aria-expanded="false" signals to screen readers that child items exist and whether they are currently visible. aria-haspopup="true" communicates that a sub-navigation is expandable. The roving tabindex (only the active item holds tabindex="0", all others -1) enables arrow-key navigation within the menu.

Escape closes the sub-menu and returns focus to the triggering top-level item. That is ARIA spec behaviour, not optional polish.

Touchpoint 3: Search Autocomplete

The Gap

Search autocomplete is one of the more complex keyboard touchpoints because it has two competing focus states: the input field holds focus, but the "active" suggestion can change via arrow-key navigation without focus leaving the input.

The typical gap: suggestions are highlighted visually, but the screen reader announces the input field, not the active suggestion. Keyboard users do not know which suggestion is currently selected.

WCAG 2.2 SC 4.1.2 (Name, Role, Value) applies: the input's programmatic value does not communicate what is active in the suggestion list.

The Pattern: Combobox with aria-activedescendant

The ARIA Combobox pattern (https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) solves exactly this. Focus stays in the input, but aria-activedescendant points to the currently active listbox item:

<label for="search-input">Search products</label>
<input
  id="search-input"
  type="text"
  role="combobox"
  aria-expanded="true"
  aria-autocomplete="list"
  aria-controls="search-listbox"
  aria-activedescendant="suggestion-2"

<ul id="search-listbox" role="listbox" aria-label="Search suggestions"> <li id="suggestion-1" role="option" aria-selected="false">Nike Air Max</li> <li id="suggestion-2" role="option" aria-selected="true">Nike Air Force</li> <li id="suggestion-3" role="option" aria-selected="false">Nike React</li> </ul> ```

When the user presses arrow-down, JavaScript updates aria-activedescendant to the next item's ID and toggles aria-selected="true" on that item. The screen reader announces the active item without focus leaving the input.

aria-expanded="true" communicates that the suggestion list is open. When the list collapses, set aria-expanded="false" and remove aria-activedescendant.

This pattern implementation is a one-time investment per storefront framework. Once built as a component, it covers all brands and all locales without additional per-brand work.

Touchpoint 4: Mini-Cart

The Gap

The mini-cart overlay has two distinct accessibility failures. First: when a product is added to the cart, there is often no accessible notification. A visual toast appears for three seconds - screen reader users get nothing.

Second: when the mini-cart overlay opens, the same focus management failure as the filter drawer occurs - focus does not move into the overlay, and the user has to navigate there manually.

WCAG 2.2 SC 4.1.3 (Status Messages) is the central criterion: status messages must be programmatically determinable without requiring focus to move to them.

The Pattern: Live Region for Cart Updates

ARIA Live Regions handle status messages without focus relocation. A role="status" or aria-live="polite" container announces updates to the screen reader as soon as they are written to the DOM:

<!-- Permanently in the DOM, always empty until an update fires -->
<div
  role="status"
  aria-live="polite"
  aria-atomic="true"
  class="sr-only"
  id="cart-live-region"
>
</div>
function announceCartUpdate(productName, quantity) {
  const liveRegion = document.getElementById('cart-live-region');
  // Clear first, then re-populate - triggers the screen reader
  liveRegion.textContent = '';
  requestAnimationFrame(() => {
    liveRegion.textContent =
      `${productName} added to cart. Cart now contains ${quantity} item${quantity !== 1 ? 's' : ''}.`;
  });
}

aria-atomic="true" ensures the screen reader announces the complete live region content, not just the changed fragment. The requestAnimationFrame trick is necessary because some screen readers ignore a DOM change when text is overwritten directly in one step.

The live region supplements the visual toast - it does not replace it. Both can coexist.

Touchpoint 5: Account Switch (Multi-Brand Storefronts)

The Gap

Account switch components in multi-brand storefronts have a specific problem: when the user switches between brands or accounts, a new page or content region loads. Keyboard navigation typically restarts from the top - the user has to tab through the entire navigation structure again to reach the actual content.

In single-brand storefronts this is a well-known friction point. In multi-brand storefronts it compounds: navigation structure can vary between brands, so the user has no reliable mental map of how many tabs it takes to reach the content.

WCAG 2.2 SC 2.4.1 (Bypass Blocks) mandates that users have a mechanism to skip repeated blocks of navigation.

The Pattern: Skip Link System for Multi-Brand Contexts

Skip links are technically straightforward - their application in multi-brand storefronts requires a bit more thought than a single generic "skip to content" link:

<!-- At the start of <body>, before any navigation -->
<nav class="skip-links" aria-label="Skip navigation">
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <a href="#search" class="skip-link">Skip to search</a>
  <a href="#account-nav" class="skip-link">Skip to account menu</a>
</nav>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #702CCE;
  color: white;
  padding: 8px;
  z-index: 100;
  transition: top 0.1s;

.skip-link:focus { top: 0; } ```

In multi-brand contexts, an additional skip link activates on brand switch:

<a href="#brand-main-content" class="skip-link"
   aria-label="Skip to [Brand Name] content">
  Skip to [Brand Name] content
</a>

When JavaScript executes the brand switch, focus should be set programmatically to #brand-main-content - following the same pattern used for client-side routing in single-page applications.

This component is a clean example of frontend-layer configuration: a skip link template with a brand name variable, built once, instantiated for n brands without a backend deployment.

Touchpoint 6: Cookie Banner

The Gap

Cookie banners have their own chapter in BFSG compliance because they are typically the first element a user encounters after page load - or rather, should encounter.

The gap has two parts. First: the cookie banner renders into the DOM but focus does not automatically move to it. Keyboard users have to navigate to the banner manually. If they skip it, they never accept or decline cookies - or they end up inadvertently interacting with content behind an overlay that was supposed to require a decision first.

Second: when the cookie banner closes (accept or decline pressed), focus goes nowhere. It often lands on body or on an overlay container that has just been removed from the DOM. The user has to restart navigation from scratch.

WCAG 2.2 SC 2.1.1 (Keyboard) and SC 2.4.3 (Focus Order) apply. Since cookie banners often overlay content and make it unreachable for keyboard users while open, SC 2.1.2 (No Keyboard Trap) is also relevant.

The Pattern: Focus-Initial with Defined Return Target

The cookie banner should take focus immediately on appearance. This prevents keyboard users from inadvertently interacting with content behind the overlay:

function showCookieBanner(bannerEl) {
  bannerEl.removeAttribute('hidden');
  bannerEl.removeAttribute('inert');
  // returnTarget is the first focusable page content after the banner
  bannerEl._returnTarget = document.getElementById('main-content')
    || document.querySelector('main')

// Brief delay for CSS transition if the banner fades in requestAnimationFrame(() => { const firstButton = bannerEl.querySelector('button'); firstButton?.focus(); }); }

function dismissCookieBanner(bannerEl, accepted) { // Save preference saveCookiePreference(accepted); // Hide the banner bannerEl.setAttribute('hidden', ''); // Return focus to a defined location bannerEl._returnTarget?.focus(); } ```

<div
  id="cookie-banner"
  role="dialog"
  aria-modal="false"
  aria-labelledby="cookie-banner-title"
  aria-describedby="cookie-banner-desc"
>
  <h2 id="cookie-banner-title">Cookie Preferences</h2>
  <p id="cookie-banner-desc">
    This website uses cookies for analytics and personalisation.
    You can accept all cookies or allow essential cookies only.
  </p>
  <button type="button" onclick="dismissCookieBanner(this.closest('[role=dialog]'), false)">
    Essential cookies only
  </button>
  <button type="button" onclick="dismissCookieBanner(this.closest('[role=dialog]'), true)">
    Accept all
  </button>
</div>

role="dialog" with aria-modal="false" is the deliberate choice over aria-modal="true". A cookie banner is not a true modal dialog - by GDPR design principle, the user should be able to leave the page without interacting with it. aria-modal="false" lets screen reader users read the full page without being trapped in the dialog.

What These Six Patterns Share

None of these patterns require backend changes. No database deploy, no API rewrite, no replatforming project. These are frontend-layer configurations: focus management, ARIA attributes, live regions, skip links.

That has a direct implication for storefronts running on a composable frontend layer. These patterns are configurable per brand and per locale. A focus-initial pattern for the cookie banner gets defined once as a component option - every brand using that component gets the pattern automatically. No brand-by-brand patch cycle.

Reference implementations for all ARIA patterns described here are documented in the W3C ARIA Authoring Practices Guide: https://www.w3.org/WAI/ARIA/apg/patterns/. The WCAG 2.2 Success Criteria are available at https://www.w3.org/TR/WCAG22/.

For the BFSG one-year review and checkout-specific patterns: Checkout Form Accessibility and BFSG Conversion 2026.

For a systemic view of compliance gaps in German shops after one year: BFSG One-Year Mark 2026.

Priority Order for the Next Sprint

If you cannot address all six patterns simultaneously, here is a triage by BFSG risk and implementation effort:

High risk, manageable effort: Cookie banner (Touchpoint 6) and mini-cart live region (Touchpoint 4). Cookie banner focus management is 20-30 lines of JavaScript. The live region is a static DOM container plus three JavaScript lines per cart update. Both are fully isolated with no cross-component impact.

Medium risk, medium effort: Search autocomplete combobox pattern (Touchpoint 3). The pattern is well documented and established, but requires cleaner separation between input state and listbox state than many existing implementations currently have.

Medium risk, higher effort: Filter drawer (Touchpoint 1) and mega-menu (Touchpoint 2). Both require structural rework if they were built without ARIA semantics. Check inert browser support in your target matrix; a polyfill is available for older environments.

Context-dependent: Account switch skip links (Touchpoint 5) are most relevant for multi-brand setups. Single-brand storefronts still benefit from a basic skip link system - the implementation cost is minimal.

---

BFSG compliance is not an edge-case concern a year in. Checkout is patched - and checkout was never the only keyboard flow. The six touchpoints here are the rest. Fixing them improves compliance posture, and it improves mid-funnel conversion for everyone who navigates without a mouse - by habit, by motor constraint, or because the trackpad is temporarily out of reach.

---

Further reading: - Composable Visual Page Builder - configure accessibility components once, all brands benefit. - Composable Headless Frontend - iterate frontend-layer patterns without backend deployment. - Performance and Core Web Vitals - accessibility and performance as connected storefront qualities. - Checkout Form Accessibility and BFSG Conversion 2026 - the checkout touchpoint as the starting point. - BFSG One-Year Mark 2026: Compliance Gaps - the systemic overview.

Read more

Frontend insights for you

Book a demo mobile
Contact

Your next level starts here.

No complex setups, no performance slowdowns. Regain full control over your digital customer experience.