← Back to blog

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

Basic Tabs Structure
<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

  1. Forgetting focus management: Don’t let keyboard users get “trapped”
  2. Stale states: Always update aria-selected when a tab is clicked.
  3. Breaking patterns: Don’t reinvent the wheel; stick to the arrow-key navigation users expect.
  4. 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

Name and email are required. Your email won't be published.

Not published. Used only for moderation.