Sunday, November 27, 2016

Semantic Tab Panels via HTML Tables

There are plenty of tutorials online for creating tabbed panels in HTML documents, some using JavaScript, some using pure CSS tricks. Most of the approaches seem to assume that a list of some type is the appropriate element to use for a tabbed interface, but I'd argue that lists are not semantically descriptive of tabbed interfaces. They either include the main tab content along with the tab name, like so:

<ul class="tabs">
    <li class="tab-active"><a href="">Active</a> Active tab content goes here</li>
    <li><a href="">Second</a> Inactive second tab content goes here</li>
    <li><a href="">Third</a></li>
</ul>

Or the tab content has no semantic relationship with the tabs that control their display, like:

<ul class="tabs">
    <li class="tab-active"><a href="#first">Active</a></li>
    <li><a href="#second">Second</a></li>
    <li><a href="#third">Third</a></li>
</ul>
<div class="tab-body">
    <div id="first">Active tab content</div>
    <div id="second">Second tab content</div>
    <div id="third">Third tab content</div>
</div>

It's simply bizarre to mix the tab name with the main tab content as with the first case. The tab name controls interaction with the tab content, it's not part of the tab's content. They are semantically distinct, and so should be distinct in the markup.

In the second case, name and content are separated, but not in a semantically meaningful way, which makes it invisible to screen readers unless you add extra annotations. The connection between tab name and tab content is completely indirect since it depends upon interpreting the meaning of a link's target as referring to an element in the current document. This is obviously only one approach to separating tab name from content, but every approach I've seen so far makes a similar tradeoff: no technique expresses the relationship between tab name and tab content using markup that properly describes their relationship in a semantically meaningful way.

So what is the semantic relationship between tab name and tab content? Well clearly the tab name is a heading of some kind that summarizes and/or meaningfully describes the content it references.

Secondly, a set of tabs are logically related in some larger context, and so markup that also expresses a grouping is the most semantically meaningful. For instance, expressing tab content as a set of divs doesn't express this association, nor does expressing tab names as standard headings H1..H6.

However, there is an HTML element that provides headings and semantically associates those headings with its contents: tables!

<table class="tabs">
    <thead><tr>
        <th class="active" id="first">Active</th>
        <th id="Second">Second</th>
        <th id="Third">Third</th></tr>
    </thead>
    <tbody>
        <tr>
            <td headers="first">Active tab content</td>
            <td headers="second">Second tab content</td>
            <td headers="third">Third tab content</td>
        </tr>
    </tbody>
</table>

The table headings are the tab names, and the table cells contain its contents. Each cell designates the header/tab name to which it belongs, which is meaningful semantic markup for tabbed interfaces. Now all that remains is providing the interactive nature of tabbed interfaces. I think most of the standard techniques will work just as well here, but I used a simple javascript function and some basic CSS you can see in this jsfiddle. Basically, the CSS sets the backgrounds, borders and mouseover behaviour needed to mimic the familiar tab layout:

body {background-color:white;}
table.tabs{border:none; border-collapse:collapse}
table.tabs>thead>tr>th{text-align: left;padding:15px;position:relative;bottom:-1px;background-color:white}
table.tabs>thead>tr>th:hover{background-color:lightgrey; cursor:pointer}
table.tabs>thead>tr>th.active:hover{background-color:white}
table.tabs>thead{border-bottom:1px solid}
table.tabs>thead>tr>th.active{border:1px solid;border-bottom:none}

Then a simple javascript function run at document load progressively enhances the table to act like a tabbed interface:

<script type="text/javascript">
    function regTabs() {
        [].forEach.call(document.querySelectorAll('table.tabs td[headers]'), function (x) {
            var t = x.parentElement.parentElement; // table or tbody tracks active tab
            var th = document.getElementById(x.attributes.headers.value);
            if (th != null) {
                x.th = th;
                th.style.float = 'left';
                th.addEventListener('click', function() {
                    t.activeTab.style.display = 'none';
                    t.activeTab.th.classList.remove('active');
                    x.style.display = '';
                    th.classList.add('active');
                    t.activeTab = x;
                });
            }
            if (th == null || !th.classList.contains('active'))
                x.style.display = 'none';
            else
                t.activeTab = x;
        });
    }
    regTabs();
</script>

Basically, the headers are all floated left and the the active tab content is tracked as a property of the table. The headings capture the click event and modify the display styling property of the tab content and the active tab name/heading.

I'm no CSS or javascript wizard, so this can all probably be improved, but it suffices to make the point that mimicking tabbed interfaces using tables is a good semantic fit.

Given the use of DomElement.classList, I believe this requires IE10 or up, but it certainly could be ported to older browsers without too much difficulty.

Edit: a redditor pointed out that description lists, dl/dt/dd, are also appropriate semantic markup for tabbed panels. These would indeed work almost as well as tables, provided you don't try to use multiple dt elements for any given dd. I've created a jsfiddle demonstrating semantic tabs using description lists, but it requires strange, non-semantic arrangement of the markup to work easily.

Edit: here's a better semantic version using the <nav> tag for the tabs, since tabs are semantically a type of navigation within the current document. Each nav link directly references the id of the corresponding tabbed content. All you need to do is decorate the outer block with "tabbed" to get the dynamic tab interface.

No comments: