0 Comments


Update! I added a second demo page to highlight responsive toggling via matchMedia API.

Ah yes, the ubiquitous tabs control, a pattern no web designer can ignore. Of course it’s a markup metaphor for the old, 3x5 tabbed index cards or tabbed file folders. Although there are many variations, they all generally work the same way, (see demo). They are a great way to break up related content in to modular bite-sized chunks. When the page loads, the content of the first, (or high priority) tab should always be open in the foreground. Subsequent background tabs should display a concise and engaging heading to encourage your audience to click and view more content.

They also provide value as a UI tool for front-end design. Tabs are a great way to organize application settings and options. Also, the content associated with a given tab can be lazy-loaded via AJAX. making it particularly efficient in a mobile context.

You often find tabs relegated to a sidebar, but I like to use tabs as responsive containers for main content. I suppose there is some risk of annoying folks by forcing them to click a tab rather than just scroll down to the next section. And also there is the problem of background tabs not being printed by default.

This post addresses these concerns via a toggle action to switch from the default tabbed layout to an expanded print layout. I call it “unfurling” the tabs, an analogy to unfurling the sails of a sailboat. The unfurled action can be wired-up to occur automatically when printing. This could be accomplished via the matchMedia API, but I’ll leave that as an exercise. My goal was to create a customizable and efficient utility that can be used with most tab/tab panel widgets. The demo shows how it can be configured to work with both the Kendo UI TabStrip, and the jQuery UI Tabs control.

The HTML Tabs Pattern

Most web-based tab widgets start off with a simple pattern that uses an unordered list to house the tabs and a matching number of div’s to house the content. And typically there is an outer, wrapper div that facilitates CSS styling and JavaScript automation.

CSS is used to display the tabs horizontally and JavaScript is used to tweak element style properties to highlight the selected tab's and hide/unhide contents accordingly. Most of this is done for you automatically by the framework.

1
2
3
4
5
6
7
8
9
10
<div id=”myTabControl”>
    <ul>
        <li>Tab Heading #1</li>
        <li>Tab Heading #2</li>
        <li>Tab Heading #3</li>
    </ul>
    <div>Content for Tab #1</div>
    <div>Content for Tab #2</div>
    <div>Content for Tab #3</div>
<div>

There may be slight variations of this pattern from one framework to the next. For example, the jQuery Tabs widget requires an anchor element inside each <li> to associate a tab to its content via the content's div' id attribute, (see API for details).

Algorithm

Essentially, when a given tab (<li> element) is clicked, the content div associated with it has its display property set to "block", and the others get switched to "none". So it follows that to unfurl the tabs one only needs to set the display property to “block” for all the content div’s. And similarly, one way to determine the current furled/unfurled state is to count the number of div’s that have their CSS display property set “block”. In fact, if just one is set to display:none, then it can be assumed that the control is in its default, or "tabbed" layout. So the toggle pseudo code might look like this:

if (at least 1 content div has CSS display property = "none") then
    1) Current state is the default, tabbed/furled, so toggle to “unfurled”
    2) Hide the tabs, (not needed in unfurled mode)
else
    1) Current state is expanded/unfurled so toggle to “furled”. i.e. restore default tab widget layout
    2) Unhide/show the tabs

This approach does not rely on swapping in/out of a class to track state, (class="tabs-furled" for example). There are situations where that may be preferable; for example, if a lot of styling for the "unfurled" state is required. I chose to rely on the framework's default content styling to keep things simple. Tracking state via an iterative scan for display:none is foolproof in this scenario. And on a somewhat related point, the two frameworks I tested monkey with the element's inline style property of the content div's. So given CSS specificity, swapping in/out of CSS classes to hide/show content won't work. Inline styles, (defined in the style attribute), will override  CSS classes, (in the class attribute).

Okay so given the pseudo code above, each toggle operation will take at least 2 passes thru the content div's; once to determine state and the other to apply styles. A callback function is used to loop through content div's. The code below fulfills all looping requirements; an anonymous function is passed in to drive what's supposed to happen; see func and func(nlTabItems[i]) in lines 4 and  9.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Private function to loop thru the tab items and invoke a function for each
//    tab item. The return value is only utilized when func returns true.
function loopThruTabs(nlTabItems, func) {
    var returnValue = false;
    for (var i = 0; i < nlTabItems.length; i++) {
        contentId = nlTabItems[i].getAttribute(defaultConfig.tabItemControllerAttribute);
        content = tabStrip.querySelector("#" + contentId);
        returnValue = func(nlTabItems[i]);
        if (returnValue === true) {
            break;
        }
    }
    return returnValue;
}

"Code reuse is the Holy Grail of Software Engineering."

Douglas Crockford

What's interesting about the "loopThruTabs" function is that it serves two different purposes in sort of a nifty way. If the "func" argument is a Boolean expression, and if happens to resolve to 'true' for a particular tab, then it immediately breaks out of the the loop and returns. Alternatively, if 'func' is a block of code to execute on each tab, then it defines no "return" value and all tabs are processed because "returnValue" will resolve to "undefined".

So as a conditional, to determine state, "loopThruTabs" is invoked as follows:

1
2
3
4
5
// Call loopThruTabs with a condition to detect "furled" state. So if
//   1 hidden, (CSS display property = "none") assume "furled"
var tabsAreFurled = loopThruTabs(nlTabItems, function(tabItem) {
       return (window.getComputedStyle(content, null).display === 'none');
});

The "tabsAreFurled" variable is assigned appropriately because the second argument is an anonymous function that returns a Boolean value. The "return" statement is paramount to properly returning the conditional value. The first argument is simply a NodeList that contains the content div's. If one of the div's has display:none then true is returned, if none of the div's has display:none then false is returned.

To see how "loopThruTabs" works to process the toggling of the furling and unfurling lets first look at the "unfurl" logic, which expands all content div's.

1
2
3
4
5
6
7
// Tab control is in default, "furled" layout, so unfurl it by looping thru
//   tabs and appling "unfurling" styles to each associated content panel
loopThruTabs(nlTabItems, function(tabItem){
    for (var styleProperty in defaultConfig.visibleStyles) {
        content.style[styleProperty] = defaultConfig.visibleStyles[styleProperty];
    }
});

Straight away note that there is no ""return". It doesn't need to return anything, it just needs to set all the visibility styles so that all div's will be unfurled.

Similarly, to furl, (return to the "tabbed" state), we process all tabs, being careful not to hide the last known active tab. And again, the anonymous function that is sent as an argument, defines no return value.

1
2
3
4
5
6
7
8
9
10
11
12
// Tab contents are expanded, or "unfurled", so restore default layout by
//   looping thru tabs and appling furling/hide styles to all content panels
//   except the one associated with last, known active tab
loopThruTabs(nlTabItems, function(tabItem){
    var activeTab = (tabItem.className.indexOf(defaultConfig.activeTabItemClassName) > -1)
    if (!activeTab) {
        // Loop thru and apply hidden styles, (except for last active)
        for (var styleProperty in defaultConfig.hiddenStyles) {
            content.style[styleProperty] = defaultConfig.hiddenStyles[styleProperty];
        }
    }
});

So there you have it, hopefully a slick way to unfurl your tabs...100% client-side of course!