Grow your CSS skills. Land your dream job.

Functional CSS Tabs Revisited

Published by Chris Coyier

TL;DR Backstory

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 funzies I added some transitions to the tabs in the live demo. I got the idea from Jacob Dubail who forked the idea into a slideshow kinda thing.

View Demo   Download Files

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.

Comments

  1. Immobile
    Permalink to comment#

    “Tab Atkins”?? Surely you jest!

  2. Juanjo
    Permalink to comment#

    Hi Chris,

    I just tested the demo and the page header flickers when changing tabs, weird! (it’s not a complain)

    I’m on OSX Safari 5.1 (in Chrome it doesn’t happen)

    • Does that for me in Safari (and not Chrome) as well. It’s related to the transitions, so just ditch that if it’s a problem.

    • Permalink to comment#

      Using Chrome 13 on a Mac running Leopard and the whole page flickers black after every transition. Strange that no one else has this problem so far…

      Really simple/cool demonstration, in any case! I may use something like this very soon! (w/ js, though)

  3. Trav
    Permalink to comment#

    The problem I see why this it doesn’t work on iOS you click the tabs and nothing happens

  4. I love seeing the all-CSS solutions to different design obstacles. Doesn’t mean I always go for them. These days, for tabs I usually end up with a CSS and JavaScript method where all the content is visible (often stacked) if JavaScript is unavailable.

    Anyway, thanks for the work. It gets the mind moving :)

    • Scott
      Permalink to comment#

      CSS pretty much always sucks at usability when it comes to this sort of thing. Personally I don’t understand this fascination with using a visual display language to change functionality and behaviour on a web page. That is exactly what Javascript is intended for.

      I know I know, testing the limits of the language is great. But I often wonder, what’s the point?

    • Permalink to comment#

      +1

    • Permalink to comment#

      I second that although it is pretty “cool” trying to make this happen with CSS ;)

    • Permalink to comment#

      I agree with you in principle. I think sometimes this blog has been guilty of seeking out pure CSS solutions just because it can be done. However, aren’t tabs more of a “visual display” method than a “functionality” or a “behavior?” They are so basic and ubiquitous, it seems like it shouldn’t be necessary to bring in javascript.

    • Permalink to comment#

      @jon It’s a fair question, but I don’t think so. The ubiquity or otherwise of an interface element shouldn’t affect whether it should be implemented in one layer or another. I think the interactive aspect of tabs is always present, regardless of how common a visual element they are, and that interaction should still be handled by the behaviour layer, while it is still a separate one.

  5. Very nice. I’ve always wanted to do this kind of thing, but I just couldn’t find a way I liked it.
    Just need to add an iOS-specific menu that changes page completely.

  6. Permalink to comment#

    Thanks, Chris! What I would add to this is cursor: pointer on tab hover :)

  7. Ben Cavens
    Permalink to comment#

    In firefox the left tab shows a ‘missing’ pixel when selected. This is cause the white bottom border is active for the entire tabwidth. Setting border-bottom to zero has resolved this issue for me.

    [type=radio]:checked ~ label {

    background-color:#FFFFFF;
    border-bottom:0px; /* minor firefox glitch – left bottom of first tab has a missing borderpixel */
    z-index:2;

    }

  8. T.J. Crowder
    Permalink to comment#

    This is very cool.

    It seems to me that for tabs, though, you actually want the fragment identifier (#tab1, #tab2, etc.) for linking, and so a solution using the same technique Jenna Smith (amongst others) has used for JS-free lightboxes (like this one) should be possible. And would have the advantage of much wider browser support, right the way down to IE6 (9.7% worldwide market — but falling fast, thank the diety). Downside there (and a big one) is that it would probably only be useful for the main tabs on a page, since it does take over the fragment identifier and probably only works well when the page isn’t scrolled.

  9. Nick
    Permalink to comment#

    On Chrome 15 when changing from the demon cat tab to another tab, it transitions to the new tab, then flashes the cat picture, then shows the real content for that cat tab again. Probably best to remove the animation – for anyone – see as how there’s a bug with it in multiple browsers.

  10. Permalink to comment#

    Very Nice,
    I like the simplicity of this way of doing it.

    There are some fairly easy enhancements I can see that can be added with Javascript with this as a base.

  11. Not working in chrome 9 on windows…

    • The browser support is listed in the article above. Chrome auto-updates and is stable in v13. Do you turn auto updating off? I didn’t even know that was possible.

  12. Could you make a video of it; i mean to explain all those selectors

  13. greenz
    Permalink to comment#

    Thanks this Chris. I was just starting to implement a tab solution for my webpage and this works perfectly. Thanks for the your articles and teaching people like me how to successfully modify websites. Everyday I feel more and more like a designer thanks to good articles like this. What im am very happy about is the fact that it uses no javascript. Thanks A Bunch

  14. Not working for me on Chrome XP. It works with Chrome on Ubuntu Linux.

  15. Daquan Wright
    Permalink to comment#

    I might be able to use this for a project I’m working on, I’ll show it off if I do.

  16. Binyamin
    Permalink to comment#
  17. Wow without Javascript! That’s the way :)

  18. Furas
    Permalink to comment#

    Clever idea with checkboxes :)

    But this has problem if tabs content have different heigth.
    So I made own version without position:absolute (and clear: both, float:left) and html structure is not so nice as yours.

    I use :checked + * + label so more tabs require more + * + :(

    Here is code http://jsfiddle.net/bUjQm/18/

    • Furas
      Permalink to comment#

      Previous version won’t work with newest Chrome 13 and Safari 5.1 but I found solution – and after that I read your news about Webkit bug :)

      One of my Webkit fix: body ~ :checked {}
      (oranything ~ any_sibling_to_+_*_+ {/*empty*/})

      New code: http://jsfiddle.net/bUjQm/21/

  19. Marco
    Permalink to comment#

    the width of the tabs container can be set to anyting?
    what about overflows?

  20. This primarily works because clicking on , as a part of browser default behaviour, checks the invisible radio-box automatically.

  21. I’m *so* using this in my personal website redesign. Thanks much!

  22. Chris, this is super stuff. Keep up the good work! :)

  23. Love this, but wish I could rig up some jQuery/JavaScript to cycle through the tabs every 30 seconds or so…

  24. Gar_onn
    Permalink to comment#

    I used It with checkboxes, to make a Tree file browser.
    show the subtree if checked.

    and the brilant thing Is that the checkboxes remember their value even after they are hiden.

    great CSS-Trick , thanks

  25. Very nice and easy way to integrate sliding content in websites, just one thing if we could show hand cursor instead of arrow on tabs mouse over. I didnt try it but hope it would be possible

  26. Ben Slayton
    Permalink to comment#

    Doesn’t look like it works on iPad. Shucks.

  27. I’ve done some work before on getting tabs to work with more semantic markup, i.e. by grouping the headers and content “properly” like you do in your example: http://fittopage.org/2010/05/tabs-done-right/

    My solution still relies on JavaScript, but that has the added benefit of being cross-browser right now.

  28. Permalink to comment#

    hey Chris!

    Awesome stuff- a suggestion (although I haven’t tried it out) – what about placing the unseen tabs below the body (which should have a background on it) with z-index: -1; – that *should* solve the height issues?

    one thing – I’m using chrome 14 – and I see the radio buttons? and the tabs don’t work – the highlight changes but the content stays on tab 2.. :/

  29. Permalink to comment#

    wait.. my bad – was looking at the wrong demo there I think .. Your’s does work :D

  30. Chris
    Permalink to comment#

    Is there any way of making this work for IE8 without Javascript? I’m working for a client at the moment who would prefer the entire website to be coded javascript free. They use IE8 as their primary browser (they have lots of security issues).

  31. AJ
    Permalink to comment#

    Probably not around to answer this but I have found this code pretty handy, mostly easy for me to get to work how I need it…except for the Tabs themselves. I can’t seem to figure out how to resize the tabs, I may be reading something wrong since I am relatively new at HTML and Dohtml. The issue I am having is that when I type something; such as a long name in a tab, the tab stretches for the name. What I’d like to do; is have the name layered, as in first name on top and last name under it. I’ve tried just adding in a between names and that didn’t do anything but mess up the way the tab looked. I’ve tried adding in a height to the tabs as in height or line height and I couldn’t get that to work either. Could totally be doing this wrong or in the wrong place though….

  32. Permalink to comment#

    @AJ – To adjust the height of the tab (as in, the part you click that says “Tab1…2…3… etc”), you need to add a “display:block” line to the “.tab label” class, then add height and width attributes as normal. I think. And I don’t guarantee that won’t mess everything else up.

    This is a useful piece of CSS. I like it.

    Although pure CSS solutions to problems may look indulgent and pointless (some solutions are), one good reason for CSS tabs is within eBay listings. You’re not allowed to use JS in your listings, so CSS is my best friend in that respect. I’ve foregone CSS tabbing in my eBay listings until now due to cross-browser compatibility issues.

    But alas, whilst the above solution does better than my previous attempts (achieving the same results far more elegantly), Internet Explorer still refuses to play ball. But apparently, IE is perfect and it’s most of the world’s other major browsers that have a problem. Obviously – I mean what do Mozilla, Google, Opera, Apple et. al. know? Who needs a global standard anyway?

    And what self-respecting web monkey doesn’t enjoy coding a totally separate solution for IE every single frickin’ time they want to do something?

    OK, you found me out. I only came here to rant about Microsoft. Enjoy your serendipitous piece of information, AJ.

    :-D

  33. Scotty
    Permalink to comment#

    For information’s sake, the fix to get this to work on older versions of iOS is with an emtpy click handler, whcih you can apply to all Labels using this Javascript code:

    
        var labels = document.getElementsByTagName("LABEL");
        var labels_l = labels.length;
        for(var l = 0; l < labels_l; l++){
            if(labels[l].hasAttribute("for") && (!labels[l].hasAttribute("onclick"))){
                labels[l].onclick = function () {};
            }
        }
    
  34. Umair
    Permalink to comment#

    That solution is not working on IE8, have anyone the resolution for IE?

  35. Aaron
    Permalink to comment#

    Is there a way that I can have the tabs controll a content area befor the tabs and the content below the tabs?

    Thanks,

    Aaron

  36. Vesa Piittinen
    Permalink to comment#

    I spent some time with these tabs to fix the variable height issue, which was an interesting issue to solve. I then also got interested in fixing it to work in IE8 and below. So that means added JavaScript. However I kept the JS to a minimum and instead let CSS do the heavy lifting in IE8, and in IE7 and even in IE6.

    I even added some responsive into it with a simple media query.

    http://codepen.io/Merri/details/bytea

    I’d say the end result is now quite a well thought CSS based tabs. Adding the fallback support did also add more classes to the HTML to avoid issues in using inputboxes and labels inside, but that shouldn’t be that big of an issue to anyone really.

    The only unresolved issue here is the linkability to content in tabs, which can be a requirement in real life. Linking to tabs via hashes is done by many, but I haven’t seen many solutions that would also open the correct tab if content inside the tab is linked to. For that I have a different solution:

    http://codepen.io/Merri/details/qsdza

    Which is IE8+ only and in need of some code cleanup, but is quite solid in what it does.

    • Vesa – Your code is awesome, thank you! The one complaint is that if you have this tabbed setup somewhere lower on the page, it scrolls the browser up in Chrome.

      I’ll probably try to compensate for that in JS (going to use it in our WordPress plugin and gave credit to you), but if you know of a better way, I’m all ears!

      Great post and great discussion here.

  37. Oscar
    Permalink to comment#

    I would love to know how to make the tabs fixed width and have the text inside the tab wrap. (pretty much what AJ asked)
    Otherwise it is a lovely implementation.

    O

  38. Fernando
    Permalink to comment#

    anyone know how to work in ie8?

    Thanks

Leave a Comment

Current day month ye@r *

*May or may not contain any actual "CSS" or "Tricks".