This appears to be one of those favorite subjects of the web dev internet that just doesn't want to die despite years coming and going. CSS-Tricks highlighted the subject briefly just yesterday, but quickly reverted the post contents to something entirely different. And I think this is a good thing to do, because the post (as it was) didn't really bring anything new to the table and wasn't any better than the four years older post. That post instead had been an inspiration for me to go ahead and solve the issues that rarely get the attention they deserve.

How it's been done

The basic concept with CSS based tabs is simple: use the :checked selector to be aware of the state of <input type="radio"> elements. The most common and thus traditional way to do the HTML structure is like this:

  <div class="tabs">
    <input type="radio" name="tab-group-1" id="tab-1" checked />
    <input type="radio" name="tab-group-1" id="tab-2" />
    <input type="radio" name="tab-group-1" id="tab-3" />

    <label for="tab-1">First Tab</label>
    <label for="tab-2">Second Tab</label>
    <label for="tab-3">Third Tab</label>

    <div class="tab" id="tab-1-panel">
        ...
    </div>
    <div class="tab" id="tab-2-panel">
        ...
    </div>
    <div class="tab" id="tab-3-panel">
        ...
    </div>
</div>

Essentially the elements have been split into three blocks. The reason for the popularity of this structure comes from the historical limitations of CSS: you can't select a parent element and you can't select an earlier element. Thus the obvious solution is to place all the state holding radio inputs to the beginning of the container element. This allows styling all the elements that follow:

  #tab-1:checked ~ [for="tab-1"],
#tab-2:checked ~ [for="tab-2"],
#tab-3:checked ~ [for="tab-3"] {
    /* label selected style */
}

#tab-1:checked ~ #tab-1-panel,
#tab-2:checked ~ #tab-2-panel,
#tab-3:checked ~ #tab-3-panel {
    /* contents selected style */
}

So, it works and it's simple enough to implement. This however does go a bit against the thought of "self-contained component" and it can be annoying to implement the actual HTML output: you either need to loop three times to generate all the elements or build HTML into three string variables in a single loop. This isn't as clean as it could be.

How it would be nice to be done

This is the meat of the original CSS-Tricks' Functional CSS Tabs Revisited article:

  <div class="tabs">

   <div class="tab">
       <input type="radio" name="tab-group-1" id="tab-1" checked />
       <label for="tab-1">First Tab</label>
       <div class="panel">
           ...
       </div> 
   </div>

   <div class="tab">
       <input type="radio" name="tab-group-1" id="tab-2" />
       <label for="tab-2">Second Tab</label>
       <div class="panel">
           ...
       </div> 
   </div>

    <div class="tab">
       <input type="radio" name="tab-group-1" id="tab-3" />
       <label for="tab-3">Third Tab</label>
       <div class="panel">
           ...
       </div> 
   </div>

</div>

This structure is far more convenient for outputting HTML as it can be implemented in a single loop. It is also easier to control in JavaScript as each tab is a self-contained component.

The problem with this structure is that it isn't very obvious to style it to appear as a tabs component. The CSS-Tricks article solves the issue by positioning the tab contents absolutely, which means the content area is of fixed height. This isn't very flexible.

Is it possible to do better?

The flow

Years back the only ways to do layout in CSS were limited to roughly four techniques: inline content, absolute and relative positioning, floats and tables. Transition from the limitations of these techniques has been on the slow side and many devs tend to look forward to flexbox as the saviour - even for this tabs issue. But first lets take a look into the historical side of things to better understand why some solutions to the tabs issue haven't been commonly figured out earlier.

Inline was quite limited as it essentially just allowed for the flow of single characters and vertical alignment of images in relation to each other on a horizontal line of text. There was no inline-block available.

Absolute and relative positioning are powerful, but don't work well with dynamic contents. For example in columns it is impossible to make both sides flex to the same height.

Float was the major technique used in early CSS based designs. It is possible to hack things enough so that two columns in a row appear to be of equal height (while they really aren't). Floats also prevented things like collapsing margins of their child elements, which made things somewhat easier for those less aware of CSS' subtleties (leading to use of floats even when not necessary).

Finally, tables worked best, but before Internet Explorer 8 the use of them for layout purposes was limited to using the actual HTML elements, this being bad for semantics and goals of CSS based design.

Internet Explorer 8 is actually quite an important part of our story here, because it expanded CSS support in many important ways. The most notable improvements are inline-block and CSS tables. For a while now IE8 has been the minimum browser to support and it's time has already ended for many. But this means there is certain safety in using inline-block and CSS tables as the support in browsers has been very good for more than five years by now.

And now we can get to the meat. The interesting part about inline content is that it will keep flowing on it's own row as long as there is space left. Then there is an interesting feature with floats: if there isn't enough space for a floated element it will jump to the next row below the inline element that is before it:

And now we know enough to implement the neat HTML syntax shown above with dynamic content height! No more need to be limited by position: absolute;.

Accessibility

There are other issues with CSS tabs that are not getting enough attention. One of them is accessiblity and usability. Most often devs (and users, too) are happy when things work with the mouse. There are some people who prefer the keyboard (I'll raise my hand) and few others whose whole use of web pages depend on content being accessible via native browser features so that tools like screen readers can help them with the use.

Long story short: display: none is evil when applied to radio inputs. It is bad for both cases mentioned above: it hides content from assistive tools and also blocks keyboard usage. Natively in browsers you can use keyboard arrow keys to change between radio options and display: none; kills this feature.

Solution is simple: use alternative methods of hiding an element.

  .tab > [type="radio"] {
    clip: rect(0 0 0 0);
    position: fixed;
    z-index: -1;
}

This is probably enough for most cases: the radio element is removed from regular page flow with fixed positioning and it is made invisible by using clip's rect that is widely supported (despite being non-standard). Finally a negative z-index gives some additional confidence. If you're a bit more paranoid and want a more concrete solution then you can also set width and height to 1px and maybe opacity to zero. Note that visibility: hidden; may also block keyboard use!

White space

The bad thing about inline-block elements is that any white space between them is made visible as if they were regular characters. This is a bit annoying thing to workaround, but can be done in CSS:

  .tabs {
    font-size: 0;
}

.tab-label,
.tab-panel {
    font-size: 16px;
    font-size: 1rem;
}

Zero font size can be a bit hard to live with, because it blocks use of em and percentage based units. Use of rem is a workable solution, but due to browser support it is a very good idea to give a px fallback.

Another alternative is to simply not have any white space between tags in the generated HTML.

Accordion

Making mobile fallback for the given HTML syntax is super simple! The structure is ideal for the accordion use case and there is no trouble in implementing it. The hardest part is choosing the CSS breakpoint, which will also be the limiting factor for the width of the tab labels.

Putting it all together

By now this is already an older example of mine, but it will suffice:

It even includes tricks to make things work in IE8, IE7 and IE6, which shows how solid the foundation of this technique is.

In summary:

  1. Mobile friendly / accordion? Yes!
  2. Accessibility and keyboard? Yes!
  3. HTML can be semantic? Yes!
  4. Convenient component-like HTML structure? Yes!
  5. Wide browser support? Yes!

About the only thing missing could be a future article: how to control CSS based tabs via JavaScript and keep taking advantage of native browser features. Another hard subject to tackle would be purely CSS based transitions.


4,855 5 30