iPhone Style Tab Control with HTML, CSS and JavaScript


Works with Desktop Safari, Desktop Google Chrome, Desktop Firefox 4, Android, iPhone, iPod Touch and iPad.
Please not that this was originally designed for Android/iPhone/iPad use. It can be used on modern desktop browsers. Just note that the attached example will only work with Firefox 4 or later because it uses CSS3 transitions for navigation.

If you’ve used an iPhone or iPad, you’ve seen them. Actually, Apple uses them on the operating system. They even use them on their Website and online assets. As is usual for Apple, they take a very different approach to implementing tabs. They use segmented buttons to indicate tabs. This works especially well in the mobile space where you want the tab to stand out and be an easy target for a fat finger. It also eliminates all the futzing around to try to get all the border of tabs and tab panels to fit together properly. The panels have no borders, just their content. The segmented buttons capture your attentions as the device that toggles the content.

The interaction is simple and immediate. As soon as a user clicks or touches a segmented button, it reveals the new content below. On the iPhone, there are not even any special effects, not even a fade. It just directly switches out the data.

Let’s go over the markup for the tab control. Note that the class names it uses are important. By using standardized markup and classes, we can reuse the tab wherever we need it in our app, and initialize it with one line of JavaScript. The tab controls has two parts. The first is a div with the class “Tabs segmentedControlBase.” This is where the segmented buttons that hide and show the tab panels reside. Following this there is another div with the class “TabPanels,” with obviously holds the tab panels. Here’s the markup:

<div class="Tabs segmentedControlBase">
	<div class="button segmentedControl leftEnd tab"><span>Search</span></div>
	<div class="button segmentedControl tab"><span>Directions</span></div>
	<div class="button segmentedControl rightEnd tab"><span>Options</span></div>
</div>
<div class="TabPanels">
	<div class="tabPanel">
		<p class="rounded">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
	</div>
	<div class="tabPanel">
		<p class="rounded">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.</p>
	</div>
	<div class="tabPanel">
		<p class="rounded">Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?</p>
	</div>
</div>

As you can see, the markup is very straightforward and we can easily reuse the pattern through an application as necessary. Of course, this needs some styling so that it looks like something. The most visual part of the tab control is the set of segmented buttons which constitute the tabs. If you’re going to use these on a mobile phone, be aware the you can probably at maximum fit in four tabs. Two or three is optimal. If your tabs are cramped, hard to read or even hard to touch because there so many, you should probably rethink how you’re present your data. Maybe a drilldown menu would serve your purposes better. Realistically, the tab control is for a small number of data sets which you wish to present to the user.

Here’s the markup. Remember, the left and right ends of a segmented button set are normally rounded. They don’t have to be, but they definitely look more elegant that way. The button or buttons between the two ends are squared off.

.segmentedControlBase {
	display: -webkit-box;
	-webkit-box-orient: horizontal;
	-webkit-box-pack:justify;
	-webkit-box-sizing: border-box;
	display: -moz-box;
	-moz-box-orient: horizontal;
	-moz-box-pack:justify;
	-moz-box-sizing: border-box;
}
.button.segmentedControl {
	display: block;
	-webkit-box-flex: 1;
	-moz-box-flex: 1;
	border: solid 1px #9a9a99;
	border-left: none;
	-webkit-border-radius: 0px;
	-moz-border-radius: 0px;
	border-radius: 0px;
	text-align: center;
	background-image: 
		-webkit-gradient(linear, left top, left bottom, 
			from(#fbfbfb), 
			to(#bdbdbd));
	background-image: 
		-moz-linear-gradient(top, 
			#fbfbfb, 
			#bdbdbd);
	color: #6b6b6b;
	font-size: 16px;
	padding: 10px;
}
.button.segmentedControl:hover, .button.segmentedControl.hover, .button.segmentedControl.selected {
	background-image: 
		-webkit-gradient(linear, left top, left bottom, 
			from(#2a55b1), 
			to(#6297f2));
	background-image: 
		-moz-linear-gradient(top, 
			#2a55b1, 
			#6297f2);
	color: #fff;
	border-color: #193a7f;
}
.button.segmentedControl.leftEnd {
	-webkit-border-top-left-radius: 10px;
	-webkit-border-bottom-left-radius: 10px;
	-moz-border-radius-topleft: 10px;
	-moz-border-radius-bottomleft: 10px;
	border-top-left-radius: 10px;
	border-bottom-left-radius: 10px;
	border-left: solid 1px #9a9a99;
}
.button.segmentedControl.rightEnd {
	-webkit-border-top-right-radius: 10px;
	-webkit-border-bottom-right-radius: 10px;
	-webkit-border-top-right-radius: 10px;
	-webkit-border-bottom-right-radius: 10px;
	border-top-right-radius: 10px;
	border-bottom-right-radius: 10px;
}
/* Styles for Tab states */
.TabPanels .tabPanel {
	display: none;
}
.TabPanels .tabPanel.selected {
	display: block;
}
/* End Styles for Tab states */

This will give us a tab control that looks like this:

tabs

Now we’re going to create the JavaScript to make the tabs function. I’m going to use the ChocolateChip JavaScript framework because it’s tiny, just 12k, and does everything I need it to do in the mobile space. For those of you who would rather use jQuery, you should be able to replicate my control’s functionality fairly easily. If you don’t know jQuery good enough to do that, then maybe you want to give ChocolateChip a look. If you compare the two, you’ll see that ChocolateChip and jQuery code look very similar. The difference is that with ChocolateChip you’re always dealing with real JavaScript. ChocolateChip gives you some simple helpers to make it easier for you to write clean, maintainable and legible code. Yet everything ChocolateChip returns is normal JavaScript. With jQuery, you’re always dealing with an object wrapper and therefore have to do things the jQuery way. You cannot use normal JavaScript with what the jQuery returns.

Anyway, here’s the code that creates the tab:

/** 
* 
* A method to initialize a set of tabs using lozenge or segmented buttons to toggle data sets in a view. It takes one argument, a unique selector identifying the view or section where the tabs reside.
*
* @method
* 
* ### setupTabs
*
* syntax:
*
*  $.setupTabs(tabsSelector);
*
* arguments:
* 
*  - string: string A valid selector for the parent of the tab control.
* 
* example:
*
*  $.setupTabs("#buyerOptions");
*
*/
$.setupTabs = function( viewSelector ) {
	var tabsSelector = viewSelector + " .Tabs .tab";
	var panelsSelector = viewSelector + " .TabPanels .tabPanel";
	var tabs = $$(tabsSelector);
	var panels = $$(panelsSelector);
	
	tabs.forEach(function(tab) {
// Set the first tab and tab panel to the selected state automatically.
		tabs[0].addClass("selected");
		panels[0].addClass("selected");
		tab.bind("click", function() {
			var i = 0, len = tabs.length;
			var panelToHide = null;
			while(i < len) {
				tabs[i].removeClass("selected");
				panels[i].removeClass("selected");
				if (this == tabs[i]) {
					panelToHide = i;
				}
				i++
			}
			this.addClass("selected");
			panels[panelToHide].addClass("selected");
		});
		
		tab.bind("touchstart", function() {
			var i = 0, len = tabs.length;
			var panelToHide = null;
			while(i < len) {
				tabs[i].removeClass("selected");
				panels[i].removeClass("selected");
				if (this == tabs[i]) {
					panelToHide = i;
				}
				i++
			}
			this.addClass("selected");
			panels[panelToHide].addClass("selected");
		});
	});
};

If you look at the argument passed in, setupTabs expects a selector indicating the parent element that the tab control abides in. This can be the article tag, the section tag, or whatever you choose. I wouldn’t expect there to be more than one set of tabs in a particular view. If no selector is supplied, the script searches the entire document for the first occurrence of the tab control’s classes and initializes it. So, if you do have more than one tab control in your mobile app, you’ll need to pass in a selector indicating where it resides.

Since by default the tab control’s CSS sets the segmented buttons to unselected and the tab panels to hidden, the script does some initialization to select the first tab and show it’s tab panel. You don’t have to tell the script how many tabs there are or how many tab panels there are. It searches the markup and figures that out itself.

Now, its time to discuss the data in the tabs. How do you intend to get it in them? The simplest way is to do it on page load. If you have many tab controls, and your app is large, you might want to avoid that. In that case you code do something creative, such as binding an event listener to the transition effect of the view the first time the user navigates to it. You could use the webkitAnimationStart event to load your data with an Ajax call. You’d only want to do this the first time the user comes to the view, so you might want to use either a custom object or a value registered in the sessionStorage object to indicate that the user has visited the section already so you don’t need to reload the data.

You can see this tab control live, or download the code to pick it apart.

Update 9/1/10
What was I thinking? My bad. The two classes “leftEnd” and “rightEnd” are totally unnecessary. Using CSS3 selectors we can always target the first and last tabs using :first-of-type and :last-of-type. That way, you never have to worry about naming the first and last. Just put the tabs and the CSS will create the first and last. So the line:

.button.segmentedControl.leftEnd

should be:

.button.segmentedControl:first-of-type

and the line:

.button.segmentedControl.rightEnd

should be:

.button.segmentedControl:last-of-type

The online sample and downloadable code has been updated for this.