Progressive Enhancement Techniques 3: the JavaScript
In my previous articles we built a tab control using Plain Old Semantic HTML with unobtrusive CSS styling. This is the final article in a three-part series that illustrates how to build a simple tabbed box using progressive enhancement techniques.
Shaky Scripting
JavaScript is the most risky web page layer. HTML support is guaranteed, CSS is likely, but JavaScript may be unavailable or disabled. Whether that affects your site will depend on your audience but, in general, you can expect 5% of users to not have scripting enabled. That’s a large percentage — certainly higher than the market share of Safari, Chrome or Opera.
There are also differences in browser implementations. Fortunately, JavaScript is a versatile language and we can fix compatibility issues. For example, IE5 does not provide the Array push() method, but we can detect when it’s missing and add it ourselves:
if (!Array.prototype.push) {
Array.prototype.push = function(item) { this[this.length] = item; };
}
Note: adding new properties or methods to native objects is a little risky, but that’s a topic for another article.
Many JavaScript frameworks provide functions which overcome browser inconsistencies. The tab box control code shown below is built as a jQuery plugin. It provides a reusable component that can be implemented on any page and jQuery will help with the more mundane aspects of client-side coding, such as DOM manipulation and event delegation.
You should be aware that jQuery and other libraries offer limited browser support. Although our tab box could work in IE5 and IE5.5, jQuery does not support those browsers and scripting will fail. It’s not a major problem in this example because the tab box operates without JavaScript. However, you should always check your requirements against your chosen library’s limitations before using it.
Finally, jQuery aficionados will know that tabs are already supported in the UI API and many other pre-built controls are available. Most will be better than this example — the code is purposely kept short to demonstrate progressive enhancement techniques.
JavaScript-enabled CSS
Without JavaScript, our tab box uses a scrolling content section. Our script must remove the scrollbars, but we can still specify this using JavaScript-controlled CSS:
/* tab control CSS: JavaScript progressive enhancements */
.tabcontent.active
{
overflow: hidden;
}
The plugin will therefore apply a class of “active” to the outer content div
when the tab box is initialized.
Developing the jQuery TabControl Plugin
The full code is available in jquery.tabcontrol.js. When the page has loaded, the JavaScript looks for any tag with a class of “tabs” and creates a new instance of a TabControl object for each node found. The node is assigned a TabControlInitialized property to prevent two or more objects pointing at the same HTML code.
Two important variables are initialized:
- (this.) Tab property will contain the nodes of all tabs and their associated content
- (this.) Active property stores the currently highlighted tab.
// jQuery plugin definition
$.fn.TabControl = function() {
// tab control object
function TabControl(list) {
if (list.TabControlInitialized) return;
else list.TabControlInitialized = true;
this.List = list;
this.Tab = {};
this.Active = null;
In the next segment, the value of T is set to this (the current object instance) so it can be referenced within the following loop. Each tab is identified and the ID it links to is extracted (see the LinkId method below). The tab and content nodes are then stored in the Tab object:
var T = this;
// find and initialize all tabs
$("li a[href^=#]", list).each(function() {
var id = T.LinkId(this);
var content = $("#"+id);
content = (content.length > 0 ? content[0] : null);
// link/content object
T.Tab[id] = {
link: this,
content: content
};
The final part of the loop performs the following operations:
- A class of “active” is added to the content’s container (the
div
with the class “tabcontent”). This will remove the scroll bars, as described above. - A click event handler is applied to all links that reference the tab content, including the tab itself (see the TabSwitch method below).
- All tabs and content are made inactive (see the Activate method below).
- The currently active tab is determined. This will normally be the first tab unless another ID is specified in the page URL.
The active tab is then activated:
// set content holder class
if (content !== null) $(content.parentNode).addClass("active");
// event delegation
$("a[href=#"+id+"]").click(function(e) { T.TabSwitch(e) });
// deactivate tab
T.Activate(id, false);
// is tab active?
if (T.Active === null || "#"+id == location.hash) T.Active = id;
});
// show active tab/content
this.Activate(this.Active);
}
Finally, we have a three methods. The first, LinkId, returns the in-page ID extracted from a link’s href attribute (the “#” is also removed):
// returns linked ID
TabControl.prototype.LinkId = function(link) {
return link.href.replace(/.*#(.+)$/, "$1");
};
The TabSwitch method is called when a tab (or any link pointing at the tab’s content) is clicked. It stops the browser’s default action, deactivates the old tab, and reactivates the new one.
The final few lines smoothly scroll the page to the tab box if it’s off the screen when a link to tabbed content is clicked. The link jumped to the content location in normal HTML, but our code has cancelled that event and replaced it with a nicer effect.
// tab click event handler
TabControl.prototype.TabSwitch = function(e) {
e.preventDefault();
var id = this.LinkId(e.target);
if (id != this.Active) {
// hide old tab
this.Activate(this.Active, false);
// switch to new tab
this.Active = id;
this.Activate(this.Active);
}
// scroll to tab box if required
var html = $('html,body');
var lt = $(this.List).offset().top, lh = $(this.List).height();
var st = Math.max(html.scrollTop(), $("body").scrollTop()), wh = $(window).height();
if (lt < st || lt+lh > st+wh) html.animate({scrollTop: lt}, 500);
return false;
};
Finally, the Activate method takes two arguments; the tab ID and either true or false to activate or deactivate the tab and its content:
// activate or deactivate a tab
TabControl.prototype.Activate = function(id, show) {
if (this.Tab[id]) {
if (show !== false) {
$(this.Tab[id].link).addClass("active");
if (this.Tab[id].content) this.Tab[id].content.style.display = "block";
}
else {
$(this.Tab[id].link).removeClass("active");
if (this.Tab[id].content) this.Tab[id].content.style.display = "none";
}
}
};
Our progressively-enhanced tab box is now complete — view the example page. It should work in all browsers even if CSS, JavaScript or both are disabled or fail. There is little need to publish a browser support list!
Resource links:
- View the HTML, CSS and JavaScript tab control
- View the JavaScript file, jquery.tabcontrol.js
- View the HTML and CSS-only tab control
- View the CSS file, pe.css
- View the HTML-only tab control
- Download all example source code
Other parts in this series:
- Progressive Enhancement Techniques 1: the HTML
- Progressive Enhancement Techniques 2: the CSS
- Progressive Enhancement Techniques 3: the JavaScript
Related reading:
- Progressive Enhancement and Graceful Degradation: an Overview
- Progressive Enhancement and Graceful Degradation: Making a Choice
- 5 Reasons Why You Should Not Publish Supported Browser Lists
- Microsoft Office Online: a Case Against Supported Browser Lists
- Who’s Using ARIA?
- How To Develop a jQuery Plugin
- How to Build an Auto-Expanding Textarea jQuery Plugin (part 1)