Functional CSS Tabs Revisited

Avatar of Chris Coyier
Chris Coyier on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

The idea of “CSS Tabs” has been around for a long time. If you Google it, a lot of what you get is styled CSS tabs, but less stuff on the building of an actually functional tabbed area. At least, functional in the sense as we think of tabbed areas today: click a tab, see a new content area with no page refresh.

Tackling functional CSS tabs has less of a deep history. Brad Kemper was messing around with it in 2008 trying to utilize the :checked pseudo selector with radio buttons and adjacent sibling combinators. This is a really cool technique that can be utilized to do things like an expand/contract tree-style menu or visually replace form elements with graphics (pioneered by Ryan Seddon).

I personally tried messing with functional tabs a while back, and came up with seven different ways to do it. Most of them centered around the use of the :target pseudo-class selector and most of those techniques sucked. One was OK. They all had one major flaw and that was that URL hashes needed to be used, which “jumps” the page down to the element with the matching ID, and that is totally unexpected, jerky, and just a bad overall experience.

Working with the radio-button/:checked technique is way better, but there was a long-standing WebKit bug that prevented pseudo-class selectors and adjacent sibling combinators from working together. Good news! That’s fixed as of stable browser releases Safari 5.1 and Chrome 13.

So let’s get this thing done the :checked way, which I think is the cleanest way to do it for now and for the next few years.

Is that a tabby cat? GET IT?!

HTML Structure

A wrapper for the whole group, then each tab is a div that contains the radio button (for the functionality), a label (the tab), and a content div.

<div class="tabs">
    
   <div class="tab">
       <input type="radio" id="tab-1" name="tab-group-1" checked>
       <label for="tab-1">Tab One</label>
       
       <div class="content">
           stuff
       </div> 
   </div>
    
   <div class="tab">
       <input type="radio" id="tab-2" name="tab-group-1">
       <label for="tab-2">Tab Two</label>
       
       <div class="content">
           stuff
       </div> 
   </div>
    
    <div class="tab">
       <input type="radio" id="tab-3" name="tab-group-1">
       <label for="tab-3">Tab Three</label>
     
       <div class="content">
           stuff
       </div> 
   </div>
    
</div>

CSS Layout

Basically:

  1. Hide the radio buttons (we don’t need to see them, we just need them to be checked or unchecked).
  2. Float the tabs so the labels fall into a row-of-links structure.
  3. Absolutely position the content areas exactly on top of each other.
  4. When a radio button is :checked, make the adjacent content area sit on top with z-index (visually revealing it and hiding the others).
.tabs {
  position: relative;   
  min-height: 200px; /* This part sucks */
  clear: both;
  margin: 25px 0;
}
.tab {
  float: left;
}
.tab label {
  background: #eee; 
  padding: 10px; 
  border: 1px solid #ccc; 
  margin-left: -1px; 
  position: relative;
  left: 1px; 
}
.tab [type=radio] {
  display: none;   
}
.content {
  position: absolute;
  top: 28px;
  left: 0;
  background: white;
  right: 0;
  bottom: 0;
  padding: 20px;
  border: 1px solid #ccc; 
}
[type=radio]:checked ~ label {
  background: white;
  border-bottom: 1px solid white;
  z-index: 2;
}
[type=radio]:checked ~ label ~ .content {
  z-index: 1;
}

This is pretty darn lightweight CSS and it’s totally expandable to any number of tabs just by adding more “tab” divs in the HTML.

JavaScript

There isn’t any, captain!

Why this way is awesome

  • It doesn’t use :target so no page-jump-suck and back-button-hijacking.
  • It’s accessible. The weird radio buttons are hidden with display: none so screen readers won’t see them and be confused (presumably) and none of the actual content is hidden with display: none;
  • It works in Safari 5.1+, Chrome 13+, Firefox 3.6+, Opera 10+, and IE 9+. Maybe a little deeper on Chrome, Firefox, and Opera, but Safari and IE are definitely correct.

Why this way isn’t awesome

  • It requires a set height to the tabbed area which sucks. I feel like there may be a way to fix this though I just haven’t quite gotten it yet.
  • The radio button thing is a bit hacky.
  • It doesn’t have the deepest browser support (IE 9 is kinda a lot to ask). If you need deeper go JavaScript.

Get it, Got it, Good

Just for funsies, I added some transitions to the tabs in the live demo.

See the Pen
Functional CSS Tabs
by Chris Coyier (@chriscoyier)
on CodePen.

The Awesome Theoretical Future – display: stack;

As I mentioned, the radio button thing is a little hacky. It’s cool that we are now able to do this and the experience is pretty good, but the way we have to code isn’t elegant or intuitive. Tab Atkins, who writes CSS specs, thinks display: stack; is probably the future for a tabbed user interface through CSS.