Accessible tabbed content (vanilla JS)

Adds tab content behaviour with associated ARIA roles and hints.

by shakibdshy

HTML

<ul class="tabs" data-directive="tabs">
    <li><a href="#panel-01" data-behaviour="tab">Tab 1</a></li>
    <li><a href="#panel-02" data-behaviour="tab">Tab 2</a></li>
    <li><a href="#panel-03" data-behaviour="tab">Tab 3</a></li>
</ul>
<div id="panel-01">
    Tab content 1
</div>
<div id="panel-02">
    Tab content 2
</div>
<div id="panel-03">
    Tab content 3
</div>

<ul class="tabs" data-directive="tabs">
    <li><a href="#panel-04" data-behaviour="tabs">Tab 1</a></li>
    <li><a href="#panel-05" data-behaviour="tabs">Tab 2</a></li>
    <li><a href="#panel-06" data-behaviour="tabs">Tab 3</a></li>
</ul>
<div id="panel-04">
    Tab content 1
</div>
<div id="panel-05">
    Tab content 2
</div>
<div id="panel-06">
    Tab content 3
</div>

CSS

body {
    font:normal 1.25em/1.5 Arial, sans-serif;
}
:focus {
    outline:1px dotted #EF9600;
    outline-offset:1px;
}

.tabs {
	margin: 0;
	padding: 0;
	border-bottom: 1px solid #EEE;
}

.tabs > li {
	display: inline;
}

.tabs > li > a {
	display: inline-block;
	padding: .25em .5em;
	border: 1px solid #EEE;
	border-bottom: 0;
	text-decoration: none;
	background: #EEE;
}

.tabs > li > a.is-selected {
	margin-bottom: -1px;
	border-bottom: 1px solid #FFF;
	background: #FFF;
}

JavaScript

(function (name, context, definition) {
    if (typeof define === 'function' && define.amd) {
        define(definition);
    }
    else if (typeof module !== 'undefined' && module.exports) {
        module.exports = definition();
    }
    else {
        context[name] = definition();
    }
})('Tabs', this, function() {

    'use strict';

    /**
     * Tabs
     * @constructor
     * @param {HTMLElement} element
     */
    function Tabs(element) {
        var i, len;

        this.target = element;
        this.tabs = element.querySelectorAll('[data-behaviour=tab]');
        this.panels = [];

        for (i = 0, len = this.tabs.length; i < len; i++) {
            this.panels.push( document.getElementById(this.tabs[i].hash.replace('#', '')) );
        }

        if (this.selectedIndex === undefined) {
            this._init();
        }
    }

    /**
     * Init
     * @private
     */
    Tabs.prototype._init = function() {
        var i;
        var self = this;

        this.target.setAttribute('role', 'tablist');

        for (i = this.tabs.length - 1; i >= 0; i--) {
            var tab = this.tabs[i];
            var panel = this.panels[i];
            var preSelected = tab.className.match(/\bis-selected\b/);
            var selected = i === 0 || preSelected || window.location.hash.replace('#', '') == panel.id;

            tab.setAttribute('role', 'tab');
            tab.setAttribute('aria-selected', selected);
            tab.setAttribute('aria-controls', tab.hash.replace('#', ''));

            panel.setAttribute('role', 'tabpanel');

            if (selected) {
                this.selectedIndex = i;

                if (!preSelected) {
                    tab.className+= ' is-selected ';
                }
            }
            else {
                panel.style.display = 'none';
            }
        }

        this.clickHandler = function(e) {
            var target = e.srcElement || e.target;

            if (target.getAttribute('role') ==...