Global UI: Footer Intermediate 10 min read

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 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 %}
>

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

ElementKeysAction
LinkEnterFollow link
ButtonEnter, SpaceActivate
Dropdown triggerEnter, Space, DownOpen dropdown
Menu itemsUp/Down arrowsNavigate items
Menu itemsEscapeClose dropdown
Menu itemsHome/EndFirst/last item
TabTabNext focusable element
TabShift+TabPrevious focusable element

Testing Keyboard Navigation

  1. Tab through the page: Can you reach all interactive elements?
  2. Activate elements: Do Enter and Space work as expected?
  3. Navigate dropdowns: Do arrow keys work?
  4. Escape: Does it close modals/dropdowns?
  5. 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)

  1. Press Cmd + F5 to enable VoiceOver
  2. Use VO + Right/Left arrows to navigate
  3. Use VO + U to open the rotor (landmarks, links, headings)
  4. Press VO + F5 to disable

Testing with NVDA (Windows)

  1. Download NVDA (free)
  2. Use arrow keys to navigate
  3. Use H for headings, D for landmarks
  4. 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:

  1. Can you tab through all navigation items?
  2. Is there a visible focus indicator?
  3. Do dropdowns work with keyboard only?
  4. Is there a skip link?
  5. Are all nav regions labeled?
  6. Does aria-current="page" mark the active page?

Key Takeaways

  1. Use semantic HTML: <nav>, <header>, <main>, <footer>
  2. Label multiple navs with aria-label
  3. Add skip links for keyboard users
  4. Mark current page with aria-current="page"
  5. Make dropdowns keyboard accessible with proper ARIA
  6. Trap focus in modals and drawers
  7. Never remove focus styles without a replacement
  8. 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...