Tabs
A tablist with roving tabindex, arrow-key movement and a focusable panel.
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><x-wsu::tabs label="Course sections" :items="[
['id' => 'schedule', 'label' => 'Schedule'],
['id' => 'prereq', 'label' => 'Prerequisites'],
]">
<x-slot:schedule>Tuesday and Thursday, 10:00 to 11:15.</x-slot:schedule>
<x-slot:prereq>CSC 1100 with a grade of C or better.</x-slot:prereq>
</x-wsu::tabs><Tabs
v-model="active"
label="Course sections"
:tabs="[
{ id: 'schedule', label: 'Schedule' },
{ id: 'prereq', label: 'Prerequisites' },
]"
>
<template #schedule>Tuesday and Thursday, 10:00 to 11:15.</template>
<template #prereq>CSC 1100 with a grade of C or better.</template>
</Tabs>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
| Attribute | Default | What it does |
|---|---|---|
items | [] | [{ id, label }]. Each id becomes a slot name. |
label | 'Sections' | Names the tablist. |
js | false | Progressive enhancement off by default. |
Vue
| Prop | Type and default | What it does |
|---|---|---|
tabs | Array, [] | [{ id, label }]. |
modelValue | String, '' | Selected tab id. |
label | String, 'Sections' | Names the tablist. |
manual | Boolean, false | Focus 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.
| Template | Elements | Accessibility attributes | Uses |
|---|---|---|---|
| internal apps/examples/static-html/index.html | div | none recorded | 1 |
| public apps/examples/static-html/public-app.html | div | none recorded | 1 |