Skip to content

Tabs

A tablist with roving tabindex, arrow-key movement and a focusable panel.

Preview

Code

The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.

<div class="wsu-tabs" data-wsu-tabs>
  <div class="wsu-tabs__list" role="tablist" aria-label="Course sections">
    <button class="wsu-tabs__tab" type="button" role="tab"
            id="tab-schedule" aria-selected="true" aria-controls="panel-schedule" tabindex="0">Schedule</button>
    <button class="wsu-tabs__tab" type="button" role="tab"
            id="tab-prereq" aria-selected="false" aria-controls="panel-prereq" tabindex="-1">Prerequisites</button>
  </div>

  <div class="wsu-tabs__panel" role="tabpanel" id="panel-schedule" aria-labelledby="tab-schedule" tabindex="0">

  </div>
  <div class="wsu-tabs__panel" role="tabpanel" id="panel-prereq" aria-labelledby="tab-prereq" tabindex="0" hidden>

  </div>
</div>

Why it works this way

  • The panel carries tabindex="0" so a keyboard user can reach content that has no focusable child of its own. Without it the panel is unreachable and unscrollable by keyboard.
  • Automatic activation by default: arrow keys move and select together. Set manual where selecting is expensive, and then Enter or Space selects.
  • Tabs are for switching between views of the same thing. They are not navigation, and they should not change the URL unless the application also handles the back button.

Accessibility

  • role="tablist" with a name, role="tab" with aria-selected and aria-controls, role="tabpanel" with aria-labelledby (4.1.2).
  • Roving tabindex, so Tab enters and leaves the set rather than walking every tab (2.1.1).
  • Hidden panels use the hidden attribute, so they are out of the tab order.

Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.

Props

Blade

AttributeDefaultWhat it does
items[][{ id, label }]. Each id becomes a slot name.
label'Sections'Names the tablist.
jsfalseProgressive enhancement off by default.

Vue

PropType and defaultWhat it does
tabsArray, [][{ id, label }].
modelValueString, ''Selected tab id.
labelString, 'Sections'Names the tablist.
manualBoolean, falseFocus moves without selecting. Enter or Space selects.

In the markup contract

contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-tabs.

TemplateElementsAccessibility attributesUses
internal apps/examples/static-html/index.htmldivnone recorded1
public apps/examples/static-html/public-app.htmldivnone recorded1