CSS3-Only Tabbed Area

Published by Chris Coyier

MORE CURRENT ARTICLE: Functional CSS Tabs Revisited

When you think of "tabs", your mind might go right to JavaScript. Watch for a click on a tab, hide all the panels, show the one corresponding to tab just clicked on. All major JavaScript libraries tackle tabs in some way. But there is a way to accomplish this same idea with "pure CSS". Just as we did with the CSS Image Switcher, let's tackle this traditionally JavaScript project with only CSS.

Tabs: a ubiquitous design pattern

:target pseudo class selector

The major empowering concept here is the CSS pseudo selector :target. :target is used in conjunction with ID selectors. The selector will match when the current URL contains a hash-tag of that exact ID. So if the current URL is:

http://css-tricks.com/#big-bam-boom

And there was an element with that ID on the page:

<h1 id="big-bam-boom">Kaplow!</h1>

Then this selector will match:

#big-bam-boom:target { color: red; }

How does one get to a URL with such a hash tag? You have links that activate them:

<a href="#big-bam-boom">Mission Control, we're a little parched up here.</a>

Browser Support / CSS3

Normally I might end a tutorial like this with a little section on browser support. But it's rather important in this case so let's get it out of the way now. :target is considered CSS3. It has wide support across all the major current browsers. Of course I'm all abut dropping support for IE 6, so who cares if it doesn't work in that (it doesn't), but :target is also not supported in IE 7 or 8. These browsers are still very much on the radar, which puts this whole tutorial in a fun/educational category rather than a use-this-in-live-production category.

Of course, if you wanted to use it in production, one option would be to use conditional comments to add JavaScript to make them to work. We won't specifically cover that here.

Clean HTML

Let's start this out right with some nice and clean HTML markup for our soon-to-be tabbed area:

<div class="tabbed-area">

	<ul class="tabs group">
	    <li><a href="#box-one">Tab 1</a></li>
	    <li><a href="#box-two">Tab 2</a></li>
	    <li><a href="#box-three">Tab 3</a></li>
	</ul>
	
	<div class="box-wrap">
	
		<div id="box-one">
		  <!-- box two content -->
		</div>
		
		<div id="box-two">
		  <!-- box two content -->
		</div>
		
		<div id="box-three">
		  <!-- box two content -->
		</div>
	
	</div>

</div>

I'd call that perfectly clean. Even with CSS disabled, you would see a list of links each of which would jump down the page to the div with the content related to that link.

CSS

The tabs themselves we'll set up as a horizontal row of links.

.tabs { list-style: none; }
.tabs li { display: inline; }
.tabs li a { color: black; float: left; display: block; padding: 4px 10px; margin-left: -1px; position: relative; left: 1px; background: white; text-decoration: none; }
.tabs li a:hover { background: #ccc; }

When you float all the links like that, the parent will collapse, so let's chuck in the old clearfix class so we can use it on the parent ul so it has a natural height. No need for the IE 6 and 7 hacks here, since neither of those support this technique anyway.

.group:after { visibility: hidden; display: block; font-size: 0; content: " "; clear: both; height: 0; }

Now let's set up the very basic styling for the panels. There is a wrapping div for all the panels. The purpose of that is to set a relative positioning context so we can absolutely position panels inside of it. All the panels will be of equal height and width and positioned exactly on top of each other. Both panels and tabs share the same 1px border.

.box-wrap { position: relative; min-height: 250px; }
.tabbed-area div div { background: white; padding: 20px; min-height: 250px; position: absolute; top: -1px; left: 0; width: 100%; }
.tabbed-area div div, .tabs li a { border: 1px solid #ccc; }

Now the magical part that makes it work is as simple as adjusting the z-index of the panels when they are "targeted".

#box-one:target, #box-two:target, #box-three:target {
  z-index: 1;
}

Ruh-Roh... What about current tab highlighting?

What we have so far is a totally functional tabbed area. You click the tab, the corresponding content in that tab loads. Functional, but not the most helpful UI. There is no feedback at all which tab is the currently showing tab, either when the page loads or even when you click to view a different tab.

This is a fairly major hurdle. I find a way to solve it, and we'll go through that here, but it's dirty. The root of the issue is that we can't select back "up" the element tree. So if we need the panels to have ID's, the only thing we can affect in CSS is decedents of that div when it is in :target. For example:

#box-four:target a[href="#box-four"] { color: red; }

That would be a cool way to select only that particular link when that panel is active, but as of now, we can't do that because that link isn't a descendant of the panel.

The only way I've been able to solve this is to actually just make the navigation descendants of the panels. This is a bummer, because that means that each panel needs to repeat the tabs....

<div class="box-wrap">

	<div id="box-four">
        <!-- content for panel -->
      
        <ul class="tabs group">
            <li class="cur"><a href="#box-four">Tab 4</a></li>
            <li><a href="#box-five">Tab 5</a></li>
            <li><a href="#box-six">Tab 6</a></li>
        </ul>
	</div>
	
	<div id="box-five">
	    <!-- content for panel -->
	
        <ul class="tabs group">
            <li><a href="#box-four">Tab 4</a></li>
            <li class="cur"><a href="#box-five">Tab 5</a></li>
            <li><a href="#box-six">Tab 6</a></li>
        </ul>
	</div>
	
	<div id="box-six">
	    <!-- content for panel -->
	
        <ul class="tabs group">
            <li><a href="#box-four">Tab 4</a></li>
            <li><a href="#box-five">Tab 5</a></li>
            <li class="cur"><a href="#box-six">Tab 6</a></li>
        </ul>
	</div>

</div>

Very much not ideal, I know. But now that the lists are inside the panels, we can just use a "current" class on the list item that is the correct corresponding link and style that. And we'll make sure the current panels tab navigation is positioned above the panels and is "on top" when it's panel is.

.cur-nav-fix .tabs { position: absolute; bottom: 100%; left: -1px; }
.cur-nav-fix .tabs li a { 
   background: -webkit-linear-gradient(top, white, #eee); 
   background: -moz-linear-gradient(top, white, #eee); 
   background: -ms-linear-gradient(top, white, #eee); 
   background: -o-linear-gradient(top, white, #eee); 
}
#box-four { z-index: 1; }
#box-four:target .tabs, #box-five:target .tabs, #box-six:target .tabs { z-index: 3; }
.cur-nav-fix .tabs li.cur a { border-bottom: 1px solid white; background: white; }

Now with current tab highlighting!

Update

The above code, as mentioned, is definitely not a good way to go. I played with this whole idea a bunch more and the demo linked to now below has a whole bunch of different ideas for this including some decent solutions.

Demo and Download

Use at will, just be aware of the IE 7 problem. All of the HTML and CSS are right on one page, so if you want to "download" it, just copy and paste the code into an html file and save it.

View Demo

Hash it out

You'll notice in the demo that if you click a tab in the one on the left and then click on a tab in the one on the right, the area on the left will revert back to it's default slide rather than keep it's current slide. That all goes back to :target and how it's related to the hash in the URL. There is really no way around this without bringing in JavaScript, so if that's not gonna work for you, you should probably just go JavaScript from the get-go.

Also, hash tag links "jump down the page" when clicked, so also note that that when you click a tab your browser window will pop down to have that tab be the top-most element (if there is enough room to scroll down on the page). Again, no fighting that without JavaScript to my knowledge.

Other resources