Tabbed Navigation Using CSS

  1. Introduction
  2. Step 1: The structure
  3. Step 2: The image
  4. Step 3: Design
  5. Bonus: JavaScript
  6. Feedback

Introduction

Digg this!

Note: The example HTML, JavaScript and CSS of this tutorial is licensed under an MIT license and is therefore free for you to use.

Note: This tutorial has been revisioned. The tabs will now grow vertically as well as horizontally, and the tutorial now also tells you how to get a hover effect.

Hello, fellow web designer! This tutorial will teach you how to create low-bandwidth tab navigation on a web page using CSS. As an extra bonus you'll also learn how to switch tabs without loading the page more than once.

Throughout the tutorial, I try to explain most of the things I am showing you, at a somewhat basic level. I have left some things out on purpose, because I assume you can figure them out on your own (for example that the CSS property border-color: #4488ff; will change the color of the border to #4488ff [and again, I am assuming you know how an RGB color in hexadecimal works]).

This tutorial follows a few guidelines of mine, which mostly revolve around the accessibility of the web page. Here they are:

If your browser is capable of styling content, you'll notice that the text in some paragraphs stand out more. These are paragraphs that I've marked as "important", so that you can skip some text without worrying you missed anything really important. Look at the next paragraph for an example:

Now that we've got that out of the way, let's get started!

Step 1: The structure

Cooking up the HTML

If you were to make a copy of this page in your favorite HTML editor/generator, how would you do it? Hopefully not with tables, in-line styles or anything that implies the current look of the page. Sure, it works, but it generally means less compatability across media, more bandwidth spendage and less readable code. Which in turn leads to angry users, bigger bills and grumpy developers. Right now you're probably wondering what I'm suggesting instead. Well, imagine that this page is an ordinary text document. You'd want a title, a table of contents and then the content. Now you need to put that into HTML.

The title

Not relevant to this tutorial, but I'll cover it because I mentioned it. HTML has six elements that each represent a level of heading. For level one, you use <h1>. For level two, <h2>, and so on. The HTML for a title heading might be <h1>My Homepage</h1>. Easy.

The table of contents

On a web page, the navigation can be thought of as a TOC. You expect the TOC to direct you to the content you want to access.

In HTML, the element which best represents a TOC is the <ol> element. OL stands for "ordered list". There is also <ul>unordered list. You can use <ul> if your navigation doesn't have a defined order, but I'll be using <ol>. In any case, the entries in the list are represented by <li>list item.

Here's the code for making a simple ordered list:

<ol>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ol>

Result:

  1. Item 1
  2. Item 2
  3. Item 3

What we'll want is an item with a link for each section (i.e., tab). A simple TOC could look something like this:

<ol id="toc">
    <li class="current"><a href="page1.html">Page 1</a></li>
    <li><a href="page2.html">Page 2</a></li>
    <li><a href="page3.html">Page 3</a></li>
</ol>

That's it! The HTML above conveys that we want an ordered list of items that contain links to other sections of a site. You'll see that a couple of the elements above have some attributes set. Namely, the class and id attributes. The class attribute should be considered as metadata for the element it is applied to. For example, one of the list items has the class current. This tells us that that item is the one that is currently selected. The id attribute uniquely identifies a particular element. The <ol> tag has the ID toc. This lets us access it programmatically. Another feature of uniquely identified elements is that the browser scrolls to them if you add their IDs as an anchor to the URL (Example of an anchor: http://www.example.com/path/to/page.html#this_is_the_anchor).

The current list item could be uniquely identified using the id attribute because there's currently only one at a time, but since it's not a constant element and we might, for some reason, have two links (tabs) active at the same time, I chose to use the class attribute.

The content

Content is generally made up of one or more headers, some paragraphs with text and every now and then some other stuff such as images, lists, quotes etc. We'll want to encapsulate the content so that we can put other things outside it without having trouble identifying it later on. A <div> (division, section) element is good for this, because it's sole purpose is to hold a collection of elements. We'll give it an identifying class, because there might be other <div> elements on the page:

<div class="content">
    <h2>Page 1</h2>
    <p>Text...</p>
</div>

Putting it together

In order for the HTML snippets to work, we'll have to make a HTML file. I'll go through the document in parts and explain each one. Here we go:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

For the file to validate, it needs a document type. The document type specifies what rules the document must follow for it to be valid. I usually go with XHTML 1.0 Strict, but there are other document types that are more... forgiving. I won't be covering them in this tutorial, however.

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>My Homepage</title>
    <link href="design.css" rel="stylesheet" type="text/css" />
</head>

The <meta> tag holds metadata about the document. In this particular case, it holds the character encoding of the page, so that the browser doesn't have to guess. The <link> element is used for linking to external resources, in this case a stylesheet. We'll be making the stylesheet in step 3.

<body>
<h1>My Homepage</h1>
<ol id="toc">
    <li class="current"><a href="page1.html">Page 1</a></li>
    <li><a href="page2.html">Page 2</a></li>
    <li><a href="page3.html">Page 3</a></li>
</ol>
<div class="content">
    <h2>Page 1</h2>
    <p>Text...</p>
</div>
</body>
</html>

With all the HTML put together, the page should look like this: page1.html.

Save the HTML code as page1.html. In the next step, we'll be going through how to make an image for the tabs.

Step 2: The image

That's right, we'll only be dealing with one image. It doesn't matter if we have five different states for the tab, it can all be done with one image. I'll start by answering the obvious question: Why?

Splitting the image into pieces left, middle and right requires six separate images for two states. If we were to add another state, for example a hover effect, the number of images required would increase to nine. Other than extra file management and more work when editing the images, what does this mean?

Hopefully I've convinced you that putting everything into one image is much better than using several images. Moving on...

Each state will take up one row of the image and will be 500 pixels wide. The idea is that the left side of the tab will be drawn in one element, then the rest will be drawn on top of it in a child element. I will explain how in the next step. Each row is 60 pixels high. This will allow the tab to be up to 60 pixels high, but it can be smaller.

Now, you might be worried. Worried about the "500 pixel" bit of my previous paragraph. Don't be, though. We will be saving this rather big image as an 8-bit indexed PNG image, which in my case took it down to just over one kilobyte. If you know that you will never have tabs even close to 500 pixels wide, you can of course shrink it. Even 200 pixels might be enough. But thanks to compression, the difference probably won't be more than a few bytes.

Depending on your previous experience with PNG images, you may also be worried about cross-browser compatability. That's only 24-bit color PNG images; 8-bit indexed PNG images are compatible with all common browsers. You can of course use GIF if you like, but the file will be bigger.

Here's the image I made:

Save this image where you saved page1.html, or make your own image. Name it tab.png. All we have to do now is to put it to use. Continue on with step 3.

Step 3: Design

Making it spicy with CSS

First let's go over the syntax of CSS. It's pretty simple. In CSS, you first select an element by specifying a filter, then inside curly braces you specify its styles in the form name: value;. For example, you can make all <p> elements red by specifying p { color: red; }.

You can also add a . (period) after the element name and specify a class name to filter out only the elements with that particular class name. For example: p.error { color: red; } changes the color of <p class="error">Error!</p>.

It's also possible to filter by a hiearchy of elements which means you require the selected element to be inside another element. For example: div.error p { color: red; } changes the color of the <p> element in <div class="error"><p>Error!</p></div>.

That covers the CSS syntax you'll be seeing on this page. For information about CSS properties and syntax, you can read more at W3Schools, CSS2 Reference (external link).

We'll be making the CSS file design.css that is referenced by page1.html, which you made in step 1. Feel free to create design.css now and save it where you saved page1.html and tab.png, and fill it in as we go. That way you can open page1.html in your web browser and see the changes as we go through the CSS below.

The tabs

The first thing we'll do is to style the ordered list that represents our TOC (<ol id="toc">):

ol#toc {
    height: 2em;
    list-style: none;
    margin: 0;
    padding: 0;
}

So, what's happening here? I'm setting the height of the TOC (which will be our bar of tabs) to 2 em units (which is twice the height of the current font). In most browsers, this is automatically calculated because all the list items (tabs) will be 2 em units in height, but in Microsoft Internet Explorer 6 it isn't.

Next, I remove the little bullets next to the list by removing its list style.

The margin and padding are both set to 0 to avoid any spacing inside and outside the list. You can read more about how margin and padding works in CSS2 Specification, Box Model (external link).

Next we'll style the default state for each list item (tab) inside the TOC:

ol#toc li {
    background: #bdf url(tab.png);
    float: left;
    margin: 0 1px 0 0;
    padding-left: 10px;
}

The background property sets anything that has to do with the background (You can also use separate properties such as background-color). In this case, I'm setting the background color to something similar to the color of the inactive state of the tab for when the background image, tab.png, isn't available (it hasn't loaded, browser has images turned off, etc).

Each tab should float to the left. This means that instead of being placed one by one vertically, the tabs will be placed one by one horizontally, to the left of any content inside their container (the other tabs, in this case).

margin: 0 1px 0 0; means the same as margin-top: 0; margin-right: 1px; margin-bottom: 0; margin-left: 0;. I am resetting all margins of the tab because some browsers give the <li> element a margin by default. The 1 pixel to the right leaves a tiny gap between the tabs. You can adjust this value to your liking.

Last, but not least, I set the left padding to 10 pixels. This is an important value which depends on the tab image. It has to be at least as wide as the left part of the tab. Without the padding, the left part will be overwritten by the background image of the <a> element. Which brings us to...

ol#toc a {
    background: url(tab.png) 100% 0;
    color: #008;
    display: block;
    float: left;
    line-height: 2em;
    padding-right: 10px;
    text-decoration: none;
}

For the background, I'm using the same thing as for the <li> element, with added position coordinates. 100% 0 means 100% to the right, no offset vertically. Since the <a> element is inside the <li> and thus on top of it, this background image will be drawn over the background image of the <li> element. So, the only visible part of the <li> element will be the 10 pixels of padding that we specified earlier. Together, these background images form a whole tab which can be anywhere from 20 to 490 pixels wide.

Next I specify a dark blue color for the text inside the tab. Since the text is a link, we have to specify the color specifically for the <a> element, because otherwise the default color (usually clear blue) would overwrite it.

The element has display: block; because it should act as a block element, rather than an inline element (i.e. text). This means that I can give it padding, margin, width, height and all that.

Normally, float: left; is only required on the container element, but the display: block; gives MSIE6 the idea that the element should be 100% wide. Beyond making it "float" to the left (which, when there is only one element, does nothing to the position), float: left; makes the element only as wide as it needs to be to hold its content.

I give the element a line height because it's essentially the same as height as long as the text stays on one line, with the exception that it is vertically centered.

outline: none; is there simply to hide the outline around links when they have focus in certain browsers, for example Mozilla Firefox.

The padding to the right should be just as much as it was for the <li> element, so that the text will be centered. If you want more spacing to the left and/or right, you modify the left padding in the <li> element and the right padding in this one.

Phew... All that's left of the tabs now is to offset our tab image when a tab is selected and tweak the font styles a bit! Go go go!

ol#toc li.current {
    background-color: #48f;
    background-position: 0 -60px;
}

For the currently selected tab, I'm changing the background color to something that fits the color of the active state of the tab. More importantly, I'm changing the offset, or position, of the background image to move 60 pixels up. This is because one row in the tab image was 60 pixels high, and I want the second row, so I move the image up, revealing its lower part.

Notice that I'm not using the background shortcut here. That's because I only want to override some of the properties, and leave the background-image property as it was.

ol#toc li.current a {
    background-position: 100% -60px;
    color: #fff;
    font-weight: bold;
}

Same thing as the last one, except this time it's the right part of the tab. I've also set the text to be bold and white.

We're almost there...

Now we just have to finish off by styling the content container we put in our HTML code earlier (<div class="content">).

div.content {
    border: #48f solid 3px;
    clear: left;
    padding: 1em;
}

First we give it a border. border: #48f solid 3px; is shorthand for border-color: #4488ff; border-style: solid; border-width: 3px;.

The next property, clear: left; tells the browser to not let any element with float: left; to float next to it. Since our list items have float: left;, the content (which comes after the tabs) must have clear: left; or it would be placed to the right of the tabs, rather than below them.

padding: 1em; gives the content a padding of 1 * [current font height].

The result

Okay so we've styled all the elements now. Didn't need much CSS for that now did we? Let's put the CSS together into design.css and see what our HTML looks like now: page1.html. Tabs! I've added page2.html and page3.html so you can click them too. Each page has the current class set for the appropiate <li> element. Here's all the CSS in one go for a better overview:

ol#toc {
    height: 2em;
    list-style: none;
    margin: 0;
    padding: 0;
}

ol#toc li {
    background: #bdf url(tab.png);
    float: left;
    margin: 0 1px 0 0;
    padding-left: 10px;
}

ol#toc a {
    background: url(tab.png) 100% 0;
    color: #008;
    display: block;
    float: left;
    height: 2em;
    line-height: 2em;
    padding-right: 10px;
    text-decoration: none;
}

ol#toc li.current {
    background-color: #48f;
    background-position: 0 -60px;
}

ol#toc li.current a {
    background-position: 100% -60px;
    color: #fff;
    font-weight: bold;
}

div.content {
    border: #48f solid 3px;
    clear: left;
    padding: 1em;
}

Supporting hover effects

There is one more thing you need to do, if you want a hover effect on your tabs that works in MSIE6 as well as the other browsers.

Since MSIE6 only supports the :hover pseudo-class (external link) on the <a> element, we have to adapt our code to that fact. Adding the :hover pseudo-class to the <a> element right now would only give us access to change the middle and right part of the tab, not the left part, since it's in the <li> element. The solution is pretty simple: We add another element inside the <a> element. The best element for this task is the <span> element; it is made to only contain text. Here's the new HTML code for the TOC:

<ol id="toc">
    <li class="current"><a href="page1.html"><span>Page 1</span></a></li>
    <li><a href="page2.html"><span>Page 2</span></a></li>
    <li><a href="page3.html"><span>Page 3</span></a></li>
</ol>

Unfortunately, this doesn't make much sense structurally, but if you want cross-browser :hover support, this is probably the best way to do it.

Next up is adding another state to the tab image. This shouldn't need much explanation. Here's my new image:

Finally, we need to move everything down a step in the stylesheet, to accommodate for the new <span> element. Here's the whole CSS again, modified for our needs:

ol#toc {
    height: 2em;
    list-style: none;
    margin: 0;
    padding: 0;
}

ol#toc li {
    float: left;
    margin: 0 1px 0 0;
}

ol#toc a {
    background: #bdf url(tab.png);
    color: #008;
    display: block;
    float: left;
    height: 2em;
    padding-left: 10px;
    text-decoration: none;
}

ol#toc a:hover {
    background-color: #3af;
    background-position: 0 -120px;
}

ol#toc a:hover span {
    background-position: 100% -120px;
}

ol#toc li.current a {
    background-color: #48f;
    background-position: 0 -60px;
    color: #fff;
    font-weight: bold;
}

ol#toc li.current span {
    background-position: 100% -60px;
}

ol#toc span {
    background: url(tab.png) 100% 0;
    display: block;
    line-height: 2em;
    padding-right: 10px;
}

div.content {
    border: #48f solid 3px;
    clear: left;
    padding: 1em;
}

I've added ol#toc a:hover and ol#toc a:hover span for the hover effect, and moved most of the properties down one step (liaspan).

The end?

That concludes this tutorial. Please leave your feedback in the feedback section. As you might have noticed this page is using the very tabs you have learned to implement. What you might also have noticed is that you haven't actually left the page while switching tabs; you've been on the same page the whole time. This is achieved through JavaScript. Go to the bonus section to learn how.

Bonus: JavaScript

Tab switching with JavaScript

I won't go through the JavaScript code like I went through the HTML and CSS. Instead, I'll tell you what you need to do to your HTML for this to work, and provide you with the JavaScript code. The JavaScript code has been commented, so it should be fairly easy to figure out what's going on if you've used it before. But first I'll list some information about using this script:

The JavaScript works by assuming you'll have <div class="content"> elements in the page with IDs that match those of the anchors in links on the page. When a link to an anchor is clicked, the script will scan for the element with the same ID as the anchor and show it, while hiding the rest. Note that due to rendering issues with browsers, the element IDs have an underscore prepended to them, stopping the browser from scrolling to their location. If CSS is disabled, but JavaScript is enabled, this means the anchors won't scroll the page as expected.

Here's the HTML for our previous example document (page1.html), modified to work with the JavaScript:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>My Homepage</title>
    <link href="design.css" rel="stylesheet" type="text/css" />
    <script src="tabs.js" type="text/javascript"></script>
</head>
<body>
<h1>My Homepage</h1>
<ol id="toc">
    <li><a href="#Page_1">Page 1</a></li>
    <li><a href="#Page_2">Page 2</a></li>
    <li><a href="#Page_3">Page 3</a></li>
</ol>
<div class="content" id="Page_1">
    <h2>Page 1</h2>
    <p>Text...</p>
</div>
<div class="content" id="Page_2">
    <h2>Page 2</h2>
    <p>Text...</p>
</div>
<div class="content" id="Page_3">
    <h2>Page 3</h2>
    <p>Text...</p>
</div>
</body>
</html>

Here's the JavaScript. Save it as tabs.js:

// CSS helper functions
CSS = {
    // Adds a class to an element.
    AddClass: function (e, c) {
        if (!e.className.match(new RegExp("\\b" + c + "\\b", "i")))
            e.className += (e.className ? " " : "") + c;
    },

    // Removes a class from an element.
    RemoveClass: function (e, c) {
        e.className = e.className.replace(new RegExp(" \\b" + c + "\\b|\\b" + c + "\\b ?", "gi"), "");
    }
};

// Functions for handling tabs.
Tabs = {
    // Changes to the tab with the specified ID.
    GoTo: function (contentId, skipReplace) {
        // This variable will be true if a tab for the specified
        // content ID was found.
        var foundTab = false;

        // Get the TOC element.
        var toc = document.getElementById("toc");
        if (toc) {
            var lis = toc.getElementsByTagName("li");
            for (var j = 0; j < lis.length; j++) {
                var li = lis[j];

                // Give the current tab link the class "current" and
                // remove the class from any other TOC links.
                var anchors = li.getElementsByTagName("a");
                for (var k = 0; k < anchors.length; k++) {
                    if (anchors[k].hash == "#" + contentId) {
                        CSS.AddClass(li, "current");
                        foundTab = true;
                        break;
                    } else {
                        CSS.RemoveClass(li, "current");
                    }
                }
            }
        }

        // Show the content with the specified ID.
        var divsToHide = [];
        var divs = document.getElementsByTagName("div");
        for (var i = 0; i < divs.length; i++) {
            var div = divs[i];

            if (div.className.match(/\bcontent\b/i)) {
                if (div.id == "_" + contentId)
                    div.style.display = "block";
                else
                    divsToHide.push(div);
            }
        }

        // Hide the other content boxes.
        for (var i = 0; i < divsToHide.length; i++)
            divsToHide[i].style.display = "none";

        // Change the address bar.
        if (!skipReplace) window.location.replace("#" + contentId);
    },

    OnClickHandler: function (e) {
        // Stop the event (to stop it from scrolling or
        // making an entry in the history).
        if (!e) e = window.event;
        if (e.preventDefault) e.preventDefault(); else e.returnValue = false;

        // Get the name of the anchor of the link that was clicked.
        Tabs.GoTo(this.hash.substring(1));
    },

    Init: function () {
        if (!document.getElementsByTagName) return;

        // Attach an onclick event to all the anchor links on the page.
        var anchors = document.getElementsByTagName("a");
        for (var i = 0; i < anchors.length; i++) {
            var a = anchors[i];
            if (a.hash) a.onclick = Tabs.OnClickHandler;
        }

        var contentId;
        if (window.location.hash) contentId = window.location.hash.substring(1);

        var divs = document.getElementsByTagName("div");
        for (var i = 0; i < divs.length; i++) {
            var div = divs[i];

            if (div.className.match(/\bcontent\b/i)) {
                if (!contentId) contentId = div.id;
                div.id = "_" + div.id;
            }
        }

        if (contentId) Tabs.GoTo(contentId, true);
    }
};

// Hook up the OnLoad event to the tab initialization function.
window.onload = Tabs.Init;

// Hide the content while waiting for the onload event to trigger.
var contentId = window.location.hash || "#Introduction";

if (document.createStyleSheet) {
    var style = document.createStyleSheet();
    style.addRule("div.content", "display: none;");
    style.addRule("div" + contentId, "display: block;");
} else {
    var head = document.getElementsByTagName("head")[0];
    if (head) {
        var style = document.createElement("style");
        style.setAttribute("type", "text/css");
        style.appendChild(document.createTextNode("div.content { display: none; }"));
		style.appendChild(document.createTextNode("div" + contentId + " { display: block; }"));
        head.appendChild(style);
    }
}

Feedback

Digg this!

So, what did you think?

Please tell me what you think of my tutorial. Was it helpful? Did you miss something? Was I too vague? Did I write too much about anything in particular? Constructive feedback will make sure I write more and better tutorials!

Feel free to ask any questions too, but fill in your e-mail address so I can get back to you.

Required

Optional