Recipe for an accessible tab pattern
Introduction
Tabs are a common UI pattern, yet they are surprisingly tricky to implement accessibly within WordPress page builders. Unlike other interface elements, tabs do not have a native HTML equivalent (yet). To make them work for everyone, you must pay close attention to markup, JavaScript behaviour, and ARIA attributes.
Think of tabs as a space-saving combination of a table of contents and its related sections. They allow users to navigate different content areas quickly without cluttering the interface.
Anatomy of accessible tabs
Before building, it is important to understand the three core components:
- Tablist: The container for all tab triggers.
- Tab: The individual clickable element that activates a content panel.
- Tabpanel: The content section that appears when its corresponding tab is active.
For more details, refer to the ARIA APG Tabs Pattern
<tablist>
<tab>Overview</tab>
<tab>Features</tab>
<tab>Services</tab>
</tablist>
<tabpanel>Overview content…</tabpanel>
<tabpanel hidden>Features content…</tabpanel>
<tabpanel hidden>Services content…</tabpanel>
Three ways to mark up a Tablist
1. Div and Buttons (Action-focused)
This method emphasises the function of the tabs as triggers that perform an action.
<h2 id="tabs-heading">Product Information</h2>
<div role="tablist" aria-labelledby="tabs-heading">
<button role="tab" aria-selected="true" aria-controls="overview-panel" id="overview-tab">
Overview
</button>
<button role="tab" aria-selected="false" aria-controls="features-panel" id="features-tab">
Features
</button>
<button role="tab" aria-selected="false" aria-controls="services-panel" id="services-tab">
Services
</button>
</div>
<div role="tabpanel" id="overview-panel" aria-labelledby="overview-tab">
<!-- Overview content -->
</div>
<div role="tabpanel" id="features-panel" aria-labelledby="features-tab" hidden>
<!-- Features content -->
</div>
<div role="tabpanel" id="services-panel" aria-labelledby="services-tab" hidden>
<!-- Services content -->
</div>
- Pros: Native keyboard support for buttons; clear visual meaning; simpler JavaScript (buttons handle ‘Enter’ and ‘Space’ automatically on a Click event.
- Cons: Content structure is lost if JavaScript fails to load.
List markup (Semantic structure)
This uses a list as the foundation, then adds ARIA roles to define the tab relationship.
<h2 id="tabs-heading">Product Information</h2>
<ul role="tablist" aria-labelledby="tabs-heading">
<li role="tab" aria-selected="true" aria-controls="overview-panel" id="overview-tab">
Overview
</li>
<li role="tab" aria-selected="false" aria-controls="features-panel" id="features-tab">
Features
</li>
<li role="tab" aria-selected="false" aria-controls="services-panel" id="services-tab">
Services
</li>
</ul>
<div role="tabpanel" id="overview-panel" aria-labelledby="overview-tab">
<!-- Overview content -->
</div>
<div role="tabpanel" id="features-panel" aria-labelledby="features-tab" hidden>
<!-- Features content -->
</div>
<div role="tabpanel" id="services-panel" aria-labelledby="services-tab" hidden>
<!-- Services content -->
</div>
- Pros: Better represents the “list” nature of the navigation.
- Cons: Requires more complex JavaScript for keyboard handling; content is inaccessible with JavaScript.
Progressive Enhancement
This is the most resilient approach. It starts with a functional table of contents that works even if JavaScript is disabled.
Initial HTML (No JS)
<h2 id="tabs-heading">Product Information</h2>
<ul aria-labelledby="tabs-heading">
<li><a href="#overview-panel">Overview</a></li>
<li><a href="#features-panel">Features</a></li>
<li><a href="#services-panel">Services</a></li>
</ul>
<div id="overview-panel">
<h2>Overview</h2>
<!-- Overview content -->
</div>
<div id="features-panel">
<h2>Features</h2>
<!-- Features content -->
</div>
<div id="services-panel">
<h2>Pricing</h2>
<!-- Servicescontent -->
</div>
Enhanced with JavaScript
The script then adds role="tablist" and role="tab" to the <ul> and <a> elements, respectively. We use role=”presentation” on the <li> tags so assistive technology ignores the list wrapper and focuses on the tab relationship.
- Pros: Works without JavaScript (graceful degradation); highly accessible.
- Cons: More complex to build and style.
Essential keyboard interactions
Accessibility relies on meeting user expectations. Your tabs should follow these standard patterns.
Tab key
- Entering the tablist: Focus goes to the active tab (not necessarily the first one).
- Leaving the tablist: Focus moves into the active tabpanel or its first interactive element.
Arrow keys
- Horizontal layouts: Use Left/Right arrows to move between tabs. Up/Down arrows should be ignored to allow normal page scrolling.
- Vertical layouts: Use Up/Down arrows to navigate. Left/Right arrows should be ignored.
Other keys
- Space or Enter: Activates the focused tab.
- Home/End: Moves focus to the first or last tab (recommended).
ARIA attributes: The accessibility bridge
| Attribute | Purpose |
|---|---|
role="tablist" |
Identifies the container |
aria-labelledby |
Links the tablist to a visible heading |
aria-selected |
Set to “true” for the active tab, “false” for others |
aria-controls |
Links the tab to its specific content panel ID |
hidden |
Hides inactive panels from all users. |
Top Tip: Focus Management
If a tabpanel contains only text (no buttons or links), add tabindex=”0″ to the panel. This ensures screen reader users can focus on the container to read its content.
Common pitfalls to avoid
- Forgetting focus management: Don’t let keyboard users get “trapped”
- Stale states: Always update aria-selected when a tab is clicked.
- Breaking patterns: Don’t reinvent the wheel; stick to the arrow-key navigation users expect.
- Ignoring the “No-JS” experience: Always consider what happens if your script doesn’t run.
Final thoughts
Building accessible tabs is a balance of semantic HTML, proper ARIA states and robust JavaScript.
Adopt a “Shift Left” mindset: accessibility isn’t a “plug-in” to add at the end of your development cycle. It is a fundamental part of the development process. Start with clean HTML, enhance it progressively and always test your work with a screen reader.
Comments
No comments yet — be the first.
Leave a comment
Replying to .
Thanks — your comment is in
It's awaiting moderation and will appear after it's approved and the site is rebuilt.