<div class="tabbed">
<ul class="flat-list">
<li>
<a href="#section-0">
<span>Tab 1</span>
</a>
</li>
<li>
<a href="#section-1">
<svg class="icon " height="20" width="20" aria-hidden="true">
<use xlink:href="../../sprite.svg#inventory" />
</svg>
<span>Tab 2</span>
</a>
</li>
<li>
<a href="#section-2">
<span>Tab 3</span>
</a>
</li>
<li>
<a href="#section-3">
<span>Tab 4</span>
</a>
</li>
</ul>
<section id="section-">
Panel content 1
</section>
<section id="section-">
Panel content 2
</section>
<section id="section-">
Panel content 3
</section>
<section id="section-">
Panel content 4
</section>
</div>
<div class="tabbed">
<ul class="flat-list">
{{#each tabs}}
<li>
<a href="#section-{{@index}}">
{{#if icon}}
{{> @svg}}
{{/if}}
<span>{{title}}</span>
</a>
</li>
{{/each}}
</ul>
{{#> @partial-block}}
{{#each tabs}}
{{#> @tab-panel}}
{{content}}
{{/ @tab-panel}}
{{/each}}
{{/ @partial-block}}
</div>
{
"tabs": [
{
"title": "Tab 1",
"content": "Panel content 1\n"
},
{
"title": "Tab 2",
"icon": "inventory",
"content": "Panel content 2\n"
},
{
"title": "Tab 3",
"content": "Panel content 3\n"
},
{
"title": "Tab 4",
"content": "Panel content 4\n"
}
]
}
/**
* Tabbed interface
*/
@media screen {
[role="tablist"] {
padding: 0.1em 0 0;
display: flex;
}
@media (max-width: 24em) {
[role="tablist"] {
overflow-x: auto;
}
}
[role="tablist"] li + li {
margin-left: 0.25em;
}
[role="tablist"] a {
display: inline-flex;
align-items: center;
background-color: var(--white);
color: var(--body-text);
text-decoration: none;
padding: 0.5rem 1em;
box-shadow: var(--tab-shadow);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[role="tablist"] a > * {
margin-top: 0;
}
[role="tablist"] a .icon {
height: 1.2em;
width: 1.2em;
margin-right: 0.25em;
}
[role="tablist"] [aria-selected] {
background: var(--tab-accent);
color: var(--white);
}
[role="tabpanel"] {
border-top: 2px solid var(--tab-accent);
border-bottom: 2px solid var(--tab-accent);
padding: 1rem 0;
margin-top: 0;
}
[role="tabpanel"]:focus {
outline: 0;
}
}
/**
* Tabbed interface
*/
'use strict';
(function () {
// Get relevant elements and collections
const tabbed = document.querySelector('.tabbed');
// Stop if we can’t find a tabbed element
if (!tabbed) return;
const tablist = tabbed.querySelector('ul');
const tabs = tablist.querySelectorAll('a');
const panels = tabbed.querySelectorAll('[id^="section"]');
const findById = id => document.getElementById(id);
// The tab switching function
const switchTab = (oldTab, newTab) => {
newTab.focus();
// Make the active tab focusable by the user (Tab key)
newTab.removeAttribute('tabindex');
// Set the selected state
newTab.setAttribute('aria-selected', 'true');
oldTab.removeAttribute('aria-selected');
oldTab.setAttribute('tabindex', '-1');
// Get the indices of the new and old tabs to find the correct
// tab panels to show and hide
let index = Array.prototype.indexOf.call(tabs, newTab);
let oldIndex = Array.prototype.indexOf.call(tabs, oldTab);
panels[oldIndex].hidden = true;
panels[index].hidden = false;
// Update browser history API & URL
let currentTab = tabs[index].getAttribute('id');
let currentState = history.state;
history.pushState(currentState, currentTab, '#' + currentTab);
};
// Add the tablist role to the first <ul> in the .tabbed container
tablist.setAttribute('role', 'tablist');
// Add semantics are remove user focusability for each tab
Array.prototype.forEach.call(tabs, (tab, i) => {
tab.setAttribute('role', 'tab');
tab.setAttribute('id', 'tab' + (i + 1));
tab.setAttribute('tabindex', '-1');
tab.parentNode.setAttribute('role', 'presentation');
// Handle clicking of tabs for mouse users
tab.addEventListener('click', e => {
e.preventDefault();
let currentTab = tablist.querySelector('[aria-selected]');
if (e.currentTarget !== currentTab) {
switchTab(currentTab, e.currentTarget);
}
});
// Handle keydown events for keyboard users
tab.addEventListener('keydown', e => {
// Get the index of the current tab in the tabs node list
let index = Array.prototype.indexOf.call(tabs, e.currentTarget);
// Work out which key the user is pressing and
// Calculate the new tab's index where appropriate
let dir = e.which === 37 ? index - 1 : e.which === 39 ? index + 1 : e.which === 40 ? 'down' : null;
if (dir !== null) {
e.preventDefault();
// If the down key is pressed, move focus to the open panel,
// otherwise switch to the adjacent tab
dir === 'down' ? panels[i].focus() : tabs[dir] ? switchTab(e.currentTarget, tabs[dir]) : void 0;
}
});
});
// Add tab panel semantics and hide them all
Array.prototype.forEach.call(panels, (panel, i) => {
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('tabindex', '-1');
panel.setAttribute('aria-labelledby', tabs[i].id);
panel.hidden = true;
});
// Initially activate the first tab and reveal the first tab panel
tabs[0].removeAttribute('tabindex');
tabs[0].setAttribute('aria-selected', 'true');
panels[0].hidden = false;
// Switch tab via URL fragment if there is one
if (window.location.hash) {
let hash = window.location.hash.substring(1);
let hashTab = findById(hash);
let currentTab = tablist.querySelector('[aria-selected]');
switchTab(currentTab, hashTab);
}
})();
JavaScript-driven, accessible tabbed interface. Supports keyboard navigation. Tabs can show either pure text, with or without icons, as well as icons only (which should be used with caution, as with all icon-only usages).