Tabs, done right
There are a lot of JavaScript-driven tab widgets around the web. All of the major JS libraries provide a tabs widget, and this is such a common user interface pattern that the ARIA specification includes several roles (tab, tablist, tabpanel) to allow accessible description of the widget. This is a good thing, but there is a problem. All of these implementations (including the ARIA example) use the following type of markup to define tab widgets:
<div class="tabs">
<ul>
<li><a href="#panel1">Tab 1</a></li>
<li><a href="#panel2">Tab 2</a></li>
<li><a href="#panel3">Tab 3</a></li>
</ul>
<div id="panel1">
<!-- panel content -->
</div>
<div id="panel2">
<!-- panel content -->
</div>
<div id="panel3">
<!-- panel content -->
</div>
</div>
What's wrong with that? Well, the semantics are a bit off. Granted, it's not the worse example of content structure, but it's not very user friendly either: you have a list of links to content (the tabs), followed by several essentially unstructured content snippets (the panels). The tabs usually hold a meaningful title for the respective panel content, which makes sense visually since the tab and the content are rendered closely together and with visual clues indicating their relatedness once the JavaScript kicks in.
However, the tabs are not usually marked up as content headers (say, <h\*>
) since that would not create a meaningful content outline (all the headers would grouped together). Also, if you wanted to use the same markup to provide different rendering depending on context you'd be in a bit of trouble. Here are just a couple of examples where you might want to do this:
- When printing the document tabs are obviously meaningless, so we'd want to output all panel sections with the correct title above each one.
- On a touch-screen smartphone we'd want to use an accordion widget instead of tabs.
- A screen reader could avoid the whole tabbed interface nastyness
The essential problem is the disassociation between title and content: the markup has been written to suit a specific UI widget. It is possible to work around this by adding the title to each panel and selectively hiding/showing it depending on context, but that is not an elegant solution.
Doing it right
That is all very interesting, you say, but where is the elegant solution, then? Coming right up. We start with some simple, semantic markup:
<h3>One</h3>
<p>Lorem ipsum dolor sit amet.</p>
<p>Proin posuere, velit non facilisis rutrum.</p>
<h3>Two</h3>
<p>Quisque tempor ligula risus.</p>
<h3>Three</h3>
<p>Suspendisse a orci turpis.</p>
And we add some scaffolding (note the selected class to highlight the selected tab):
<div class="tabWidget">
<div class="tabPanel">
<h3 class="tab">One</h3>
<div class="panel">
<div class="panel-inner">
<p>Lorem ipsum dolor sit amet.</p>
<p>Proin posuere, velit non facilisis rutrum.</p>
</div>
</div>
</div>
<div class="tabPanel selected">
<h3 class="tab">Two</h3>
<div class="panel">
<div class="panel-inner">
<p>Quisque tempor ligula risus.</p>
</div>
</div>
</div>
<div class="tabPanel">
<h3 class="tab">Three</h3>
<div class="panel">
<div class="panel-inner">
<p>Suspendisse a orci turpis.</p>
</div>
</div>
</div>
</div>
Note that I am using <div>
s simply to keep the original semantics. You can use whatever elements make sense -- there is an example using a list on the demo page. Here is the CSS:
.tabWidget {
overflow: hidden;
}
.tabWidget .tabPanel {
display: block;
}
.tabWidget .tab {
float: left;
/* change rules below for your desired visual effect */
cursor: pointer;
border: 2px solid #000;
margin-right: 5px;
padding: 2px 8px;
}
.tabWidget .panel {
display: none;
float: right;
margin-left: -100%;
margin-top: 1.7em; /* adjust as needed */
width: 100%;
}
.tabWidget .panel-inner {
/* change rules below for your desired visual effect */
border: 2px solid #000;
padding: 10px;
}
.tabWidget .selected .tab {
/* change rules below for your desired visual effect */
background-color: #000;
color: #FFF;
}
.tabWidget .selected .panel {
display: block;
}
The magic happens in the rules float: right, margin-left: -100%, and width: 100%, applied to the .panel <div>
. These rules remove the panels from the horizontal flow of the document (since their width is cancelled by their negative margin), but leave their vertical dimension intact, allowing them to push down the bottom of the tab widget. A gotcha is that the .panel <div>
s must be pushed down by the correct amount so that they line up with the bottom of the tabs (i.e. adjust the margin-top: 1.7em rule to suit your design). This also means that multi-line tabs are probably difficult to achieve (I haven't tried them; let me know if you have ideas). The purpose of the .panel-inner <div>
is to allow for borders and/or padding to be added to the panel content without interfering with the width: 100% rule applied to .panel <div>
. If you need neither, these can be removed. All we need now is some JavaScript to handle the tab switching. Here's an example using JQuery:
$.fn.tabsDoneRight = function(options) {
return this.each(function(index, el) {
$(el).find(".tab").each(function(tabIndex, tabEl) {
$(tabEl).click(function() {
$(el).find(".selected").removeClass("selected");
$(this).parents(".tabPanel").addClass("selected");
return false;
});
$(tabEl).attr("tabindex", 0).keypress(function(ev) {
if (ev.keyCode == 13) $(this).click();
});
});
if (!$(el).find(".tabPanel:has(.selected)").length) {
$(el).find(".tabPanel").first().click();
}
});
};
$(function() {
$(".tabWidget").tabsDoneRight();
});
More accessible
We're adding onclick behaviour to elements that don't have a default interaction (the <h3>
tabs) so keyboard navigation won't automatically be added by the browser. We must make the tabs focusable by the tab key (setting the tabindex attribute), and triggering the click event handler when the enter key is pressed (see the keypress event handler in the code above). What if JavaScript is disabled? The display: none rule will keep all non-selected tab panels inaccessible, so I recommend a little trick: display all panels by default, and trigger the hiding if JS is enabled. Let's change the CSS:
.tabWidget {
overflow: hidden;
}
.tabWidget .tabPanel {
display: block;
}
.tabWidget .tab {
clear: both;
float: left;
/* change rules below for your desired visual effect */
cursor: pointer;
border: 2px solid #000;
margin-right: 5px;
padding: 2px 8px;
}
.js .tabWidget .tab {
clear: none;
}
.tabWidget .panel {
clear: right;
display: block;
float: right;
margin-left: -100%;
margin-top: 1.7em; /* adjust as needed */
width: 100%;
}
.js .tabWidget .panel {
display: none;
clear: none;
}
.tabWidget .panel-inner {
/* change rules below for your desired visual effect */
border: 2px solid #000;
padding: 10px;
}
.tabWidget .selected .tab {
/* change rules below for your desired visual effect */
background-color: #000;
color: #FFF;
}
.js .tabWidget .selected .panel {
display: block;
}
To trigger the correct CSS for JS-enabled pages, add this code as far up in the <body>
as possible:
<script type="text/javascript">
document.body.className += " js";
</script>
Putting it all together
With the above markup it should now be relatively straightforward to create alternative styling for mobile and print mediums. Even better, making use of CSS media queries you can decide to change the appearance and behaviour of the widget based, for instance, on the available viewport space. This solution has been tested in IE6, IE7, IE8, FF3.6, Opera 10, Chrome 6. It also seems to work in mobile browsers (Nokia and Sony Ericsson), although you will need to make the tabs either links or buttons (maybe via the JQuery wrap() function?) since the onclick event doesn't seem to trigger on non-link elements on those browsers.