Accessibility for Navigation Regions
Make your header and footer navigation accessible to all users with proper landmark roles, ARIA attributes, skip links, and keyboard navigation.
Accessible navigation ensures that all users, including those using screen readers, keyboards, or other assistive technologies, can navigate your store. Let’s implement the essential accessibility patterns.
Landmark Roles
HTML5 landmark elements help screen reader users understand page structure:
<body> <header>...</header> <!-- Landmark: banner --> <nav>...</nav> <!-- Landmark: navigation --> <main>...</main> <!-- Landmark: main --> <aside>...</aside> <!-- Landmark: complementary --> <footer>...</footer> <!-- Landmark: contentinfo --></body>Screen reader users can jump directly between these landmarks.
Using Semantic Elements
{# layout/theme.liquid #}
<body> <a href="#main-content" class="skip-link">Skip to content</a>
<header class="header" role="banner"> <nav aria-label="Main navigation"> <!-- header nav --> </nav> </header>
<main id="main-content" role="main"> {{ content_for_layout }} </main>
<footer class="footer" role="contentinfo"> <nav aria-label="Footer navigation"> <!-- footer nav --> </nav> </footer></body>Multiple Navigation Regions
When you have multiple <nav> elements, label them with aria-label:
<header> {# Main navigation #} <nav aria-label="Main"> <ul> <li><a href="/">Home</a></li> <li><a href="/collections">Shop</a></li> </ul> </nav>
{# Utility navigation #} <nav aria-label="Account and cart"> <a href="{{ routes.account_url }}">Account</a> <a href="{{ routes.cart_url }}">Cart</a> </nav></header>
<footer> {# Footer navigation sections #} <nav aria-label="Customer service"> <h3>Customer Service</h3> <ul><!-- links --></ul> </nav>
<nav aria-label="About us"> <h3>About</h3> <ul><!-- links --></ul> </nav>
{# Policy links #} <nav aria-label="Legal"> <ul><!-- policy links --></ul> </nav></footer>Screen reader users hear: “Main navigation”, “Account and cart navigation”, etc.
Skip Links
Skip links let keyboard users bypass repetitive navigation:
{# At the very start of body #}<a href="#main-content" class="skip-link"> Skip to content</a>
{# Or multiple skip links #}<div class="skip-links"> <a href="#main-content" class="skip-link">Skip to content</a> <a href="#footer" class="skip-link">Skip to footer</a></div>.skip-link { position: absolute; top: -100%; left: 50%; transform: translateX(-50%); padding: var(--spacing-sm) var(--spacing-md); background: var(--color-primary); color: var(--color-primary-text); text-decoration: none; z-index: 9999; transition: top 0.2s;}
.skip-link:focus { top: var(--spacing-sm);}Target Elements
Ensure skip link targets are focusable:
<main id="main-content" tabindex="-1"> {{ content_for_layout }}</main>The tabindex="-1" allows the element to receive focus programmatically without being in the tab order.
Current Page Indication
Indicate the current page in navigation:
<nav aria-label="Main"> <ul> {%- for link in menu.links -%} <li> <a href="{{ link.url }}" {% if link.active %}aria-current="page"{% endif %} > {{ link.title }} </a> </li> {%- endfor -%} </ul></nav>For parent items with an active child:
<a href="{{ link.url }}" {% if link.active %}aria-current="page"{% elsif link.child_active %}aria-current="true"{% endif %}>Dropdown Menu Accessibility
ARIA Attributes for Dropdowns
<li class="nav__item"> <button class="nav__link" aria-expanded="false" aria-haspopup="true" aria-controls="dropdown-{{ link.handle }}" > {{ link.title }} </button>
<ul id="dropdown-{{ link.handle }}" class="nav__dropdown" role="menu" aria-label="{{ link.title }} submenu" > {%- for child in link.links -%} <li role="none"> <a href="{{ child.url }}" role="menuitem" {% if child.active %}aria-current="page"{% endif %} > {{ child.title }} </a> </li> {%- endfor -%} </ul></li>JavaScript for Dropdown State
class AccessibleDropdown extends HTMLElement { connectedCallback() { this.trigger = this.querySelector('[aria-expanded]'); this.menu = this.querySelector('[role="menu"]'); this.menuItems = this.querySelectorAll('[role="menuitem"]');
this.trigger.addEventListener('click', () => this.toggle()); this.trigger.addEventListener('keydown', (e) => this.onTriggerKeydown(e)); this.menu.addEventListener('keydown', (e) => this.onMenuKeydown(e));
document.addEventListener('click', (e) => { if (!this.contains(e.target)) this.close(); }); }
toggle() { const isOpen = this.trigger.getAttribute('aria-expanded') === 'true'; if (isOpen) { this.close(); } else { this.open(); } }
open() { this.trigger.setAttribute('aria-expanded', 'true'); this.menuItems[0]?.focus(); }
close() { this.trigger.setAttribute('aria-expanded', 'false'); this.trigger.focus(); }
onTriggerKeydown(event) { switch (event.key) { case 'ArrowDown': case 'Enter': case ' ': event.preventDefault(); this.open(); break; } }
onMenuKeydown(event) { const items = Array.from(this.menuItems); const currentIndex = items.indexOf(document.activeElement);
switch (event.key) { case 'ArrowDown': event.preventDefault(); items[(currentIndex + 1) % items.length]?.focus(); break; case 'ArrowUp': event.preventDefault(); items[(currentIndex - 1 + items.length) % items.length]?.focus(); break; case 'Escape': this.close(); break; case 'Home': event.preventDefault(); items[0]?.focus(); break; case 'End': event.preventDefault(); items[items.length - 1]?.focus(); break; } }}
customElements.define('accessible-dropdown', AccessibleDropdown);Mobile Menu Accessibility
Focus Trapping
When the mobile menu is open, trap focus within it:
class MobileMenu extends HTMLElement { trapFocus() { const focusableElements = this.querySelectorAll( 'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])' );
const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1];
this.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } }); }}Hiding Background Content
When a modal/drawer is open, hide the background from screen readers:
open() { this.setAttribute('aria-hidden', 'false'); document.querySelector('main').setAttribute('aria-hidden', 'true'); document.querySelector('header').setAttribute('aria-hidden', 'true');}
close() { this.setAttribute('aria-hidden', 'true'); document.querySelector('main').setAttribute('aria-hidden', 'false'); document.querySelector('header').setAttribute('aria-hidden', 'false');}Or use the inert attribute for broader support:
open() { document.querySelector('main').inert = true;}
close() { document.querySelector('main').inert = false;}Keyboard Navigation Patterns
Expected Keyboard Behaviors
| Element | Keys | Action |
|---|---|---|
| Link | Enter | Follow link |
| Button | Enter, Space | Activate |
| Dropdown trigger | Enter, Space, Down | Open dropdown |
| Menu items | Up/Down arrows | Navigate items |
| Menu items | Escape | Close dropdown |
| Menu items | Home/End | First/last item |
| Tab | Tab | Next focusable element |
| Tab | Shift+Tab | Previous focusable element |
Testing Keyboard Navigation
- Tab through the page: Can you reach all interactive elements?
- Activate elements: Do Enter and Space work as expected?
- Navigate dropdowns: Do arrow keys work?
- Escape: Does it close modals/dropdowns?
- Focus visible: Can you always see where focus is?
Focus Styles
Never remove focus outlines without a replacement:
/* BAD: Removes focus indication */*:focus { outline: none;}
/* GOOD: Custom focus styles */:focus { outline: 2px solid var(--color-focus); outline-offset: 2px;}
/* GOOD: Only remove for mouse users */:focus:not(:focus-visible) { outline: none;}
:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px;}Screen Reader Testing
Testing with VoiceOver (Mac)
- Press Cmd + F5 to enable VoiceOver
- Use VO + Right/Left arrows to navigate
- Use VO + U to open the rotor (landmarks, links, headings)
- Press VO + F5 to disable
Testing with NVDA (Windows)
- Download NVDA (free)
- Use arrow keys to navigate
- Use H for headings, D for landmarks
- Press Insert + Q to quit
Common Issues to Check
- Are all links and buttons announced?
- Are images described (alt text)?
- Is the navigation structure clear?
- Are form errors announced?
- Do dynamic changes get announced?
Complete Accessible Navigation
{# Header navigation #}<header class="header" role="banner"> <a href="#main-content" class="skip-link">Skip to content</a>
<nav class="header__nav" aria-label="Main"> <ul class="nav__list" role="list"> {%- for link in linklists['main-menu'].links -%} {%- if link.links.size > 0 -%} <li class="nav__item"> <accessible-dropdown> <button class="nav__link" aria-expanded="false" aria-haspopup="true" aria-controls="dropdown-{{ link.handle }}" > {{ link.title }} <svg aria-hidden="true" class="nav__arrow">...</svg> </button>
<ul id="dropdown-{{ link.handle }}" class="nav__dropdown" role="menu" > {%- for child in link.links -%} <li role="none"> <a href="{{ child.url }}" role="menuitem" {% if child.active %}aria-current="page"{% endif %} > {{ child.title }} </a> </li> {%- endfor -%} </ul> </accessible-dropdown> </li> {%- else -%} <li class="nav__item"> <a href="{{ link.url }}" class="nav__link" {% if link.active %}aria-current="page"{% endif %} > {{ link.title }} </a> </li> {%- endif -%} {%- endfor -%} </ul> </nav></header>
<main id="main-content" tabindex="-1" role="main"> {{ content_for_layout }}</main>
<footer class="footer" role="contentinfo" id="footer"> <nav aria-label="Footer"> <!-- footer navigation --> </nav></footer>Practice Exercise
Audit your navigation for accessibility:
- Can you tab through all navigation items?
- Is there a visible focus indicator?
- Do dropdowns work with keyboard only?
- Is there a skip link?
- Are all nav regions labeled?
- Does
aria-current="page"mark the active page?
Key Takeaways
- Use semantic HTML:
<nav>,<header>,<main>,<footer> - Label multiple navs with
aria-label - Add skip links for keyboard users
- Mark current page with
aria-current="page" - Make dropdowns keyboard accessible with proper ARIA
- Trap focus in modals and drawers
- Never remove focus styles without a replacement
- Test with screen readers and keyboard-only navigation
What’s Next?
The final lesson covers Testing Responsiveness Across Breakpoints to ensure your layouts work on all devices.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...