Tabbed Interfaces in CSS

Greetings, list.

In the recent updated draft of Template Layout, the section on tabbed
layouts was removed.  This was a good choice, as there had been very
little discussion on how to approach this problem.  I hope to correct
that in this thread.  I will discuss existing tabbed layouts
implemented in js, explain an advanced example utilizing js-based
tabbed display, show a working example of a tabbed layout in pure CSS
(it's a hack, but it works), dissect where I believe the tabbed layout
section in the Advanced Layout Module went wrong, and finally outline
what I think the best approach to this would be.

The Problem
===========
Tabbed layouts are reasonably common in heavily js-driven sites.  They
allow you to present small bits of information, and offer further
information if requested without a server round-trip.  I personally
use a tabbed layout in *all* of the internal sites at my company, to
organize information without producing a ton of pages.  Because it
doesn't cause a proper server request, it's also very useful for when
you are presenting a form, but want to make large amounts of
information available as well

In general terms, a tabbed layout consists of a number of 'cards'
organized into a 'stack', such that only one card from a given stack
can be displayed at a time.  There are also 'tabs', which activate
particular cards, causing them to be displayed (and hiding the
currently-displayed card in that stack, if it's different).

When phrased in this way, the tabbed layout is even more prevalent
than one would immediately think, as the popular web 2.0 "accordion"
construct also matches this definition.  The only difference is that
in a traditional tabbed layout the tabs and cards are grouped
separately, while in a traditional accordion the cards are interwoven
with the tabs.

As well, a tabbed layout requires the ability to distinguish an
'active' tab (corresponding to a currently displayed card) from an
inactive tab.


Existing JS Solutions
=====================
Tabbed layouts are popular enough that they were one of the earliest
jQuery plugins produced, and are now part of the official UI plugin
stack.  (Accordion is as well.)

In the jQuery tabbed layout, the focus is on the tabs.  Cards are
simply undistinguished elements on the page, which must have #ids.
The tabs, however, have a very particular structure.  They must be
presented as a <ul>, with each <li> containing an <a> hash-linking to
a particular card.  The collection of <li>s here defines the stack.
When activated and pointed at the tabs' <ul>, the javascript hunts for
the links, associates them with the linked elements, and applies
appropriate classes to everything (tabs-container, active-tab,
inactive-tab, active-card, inactive-card, etc.) to allow you to style
appropriately.  It then attaches a few click event listeners to the
tabs so that it can swap the active/inactive classes when appropriate.

This ends up being extremely nice and flexible, and degrades well.  In
the absence of javascript, all the content is displayed, and you have
a list of links to particular sections for quick access.  The tabs and
cards can be well-separated from each other in the markup and
display(this has been very useful to me before), and everything is
fully stylable with standard CSS techniques.

It also has a slight weakness.  Since the stack is defined by the
tabs, not the cards, it's possible for a single card to be a part of
multiple stacks, which can produce confusing behavior.  However, it
also allows multiple tabs to correspond to a single tab, which *has*
been useful to me in the past.


Advanced Example of JS-based tabbed layout
==========================================
At http://www.xanthir.com/xyz-security-systems/floorplan.php you'll
see a page from a school project I did last semester.  I use tabbed
layout in a complex way to achieve the desired display behavior here.

First, the floor display is a rather straightforward tabbed display.
Two tabs, two cards, and the tabs and cards are next to each other in
the layout (though not in the markup - this is purely for display
purposes).

More complex is the device display.  First, the list on the left
constitutes tabs corresponding to the individual device setup forms on
the right.  Note that the tabs and the cards are separated in the
markup, and the tabs themselves are somewhat separated from each other
(placed in two separate lists).  More interesting are the devices on
the central floorplan, which are *also* tabs for the device setup
forms on the left.  The two sets of tabs interact appropriately, as
they cover the exact same set of cards.


Tabbed Layout in Pure CSS
=========================
At http://www.xanthir.com/css-modal.html I have implemented a tabbed
display in pure CSS.  The cards have class="card", and I use the
:target pseudoclass to display the active card.  I then employ the
ingenious hidden :checked hack (I totally can't remember who
introduced this originally) to style the active tab - the tab, in
addition to being a link (to trigger :target behavior) is also a
<label> corresponding to a hidden radio button immediately preceding
it, so I can use a ":checked + * a" selector to target the active tab.

This example is a horrible hack, and has several weaknesses (you have
to use non-semantic <input>s to style the tab, you can't create
multiple 'stacks', other hash-links (like the modal example also on
that page) interfere), but it demonstrates that the essential powers
needed to implement this are appropriately part of CSS.


Problems in Advanced Layout Module tabbed layout
================================================
The draft is no longer up, so I'll summarize how the proposal worked.

display:card elements were placed into a display:stack container,
defining their stack explicitly in the markup.  An element within the
card was given display:tab.  This causes it to be pulled out of the
card's display, and used to fill the automatically generated tab
display docked on one side of the stack.

There are several weaknesses with this draft:

1. It was a slight abuse of display (display:stack and display:card
elements were treated as display:block by default).
2. It forced the designer to put the card's stack relationship
explicitly in the markup.  In many basic tabbed displays this is fine,
but there can be legitimate reasons to *not* do this.
3. The tabs were extremely limited.  They must come from the card's
descendants, they must be next to the card stack, and their display is
essentially browser chrome, limiting the author's ability to change
the style of the tabs.
4. Tabs and cards are forced into a one-to-one relationship,
preventing an author from creating complex layouts like I outlined
above in my example.
5. Accordions are completely impossible, even though they are
functionally identical to a tabbed layout.

These weaknesses are largely a result of defensible choices, of
course.  This approach makes it easy to take existing markup and
transform it into a tabbed layout.  However, it severely restricts an
author's options when they know that a tabbed layout is intended from
the beginning.  Both this and my proposal (below) offer good fallback
behavior in the absence of CSS.


A Proposal for a Tabbed Layout
==============================
My proposal takes the flexibility of JS-based tabbed layouts and
marries it to the simplicity and rigor that CSS can bring.  It
requires only a single new property and a handful of pseudoclasses.
No pseudoelements or new values for existing properties are needed.
It revolves around the use of hash-links and ids, but without leaning
on the :target pseudoclass and with significant intelligence to help
in styling.

New Property: card-stack
------------------------
Values: none | <string>
Initial: none
Applies to: all elements
Inherited: no

The card-stack property identifies an element as a card in a tabbed
layout, and defines the stack it belongs to.  Only a single card (the
'active' card) from a given stack is displayed at a time - the rest of
the cards in the stack are treated as display:none.

A card is made 'active' by either it or one of its descendants being
expressly targetted by a hash-link.  Once a card is 'active', it
remains 'active' until another card in the same stack is made
'active'.  Cards in other stacks can be made 'active' without
affecting cards in any other stack.

Any card with a string value for card-stack matches the pseudoclass
:card.  Currently active cards also match the :active-card
pseudoclass.  ((Is an :inactive-card pseudoclass desirable?  It's not
necessary, but it may make things slightly easier - you won't have to
set inactive properties with :card and then explicitly override them
with :active-card.  It would also make defining the UA defaults easier
- just say ":inactive-card{display:none !important;}".  Alternately,
drop :card and just use :active-card and :inactive-card.))

Any link which is targetting a card on the page is considered a 'tab',
and will match the :tab pseudoclass.  A link targetting a currently
active tab will also match the :active-tab pseudoclass.  ((Again, do
we want an :inactive-tab pseudoclass instead of/in addition to the
:tab pseudoclass?))  Links targetting descendants of a card will
activate the card when followed, but do not count as tabs themselves.


Questions/Issues
----------------
Currently, *all* cards start out inactive when you enter a page
(unless the page had a hash, of course).  Most JS-based tabbed layouts
will automatically activate the first card (with 'first' defined in
some consistent manner).  Should we adopt this?  Should there be a
control for this?  How would it work?

Once a card has been activated from a stack, there's no way currently
to close all cards from that stack - you'll always have one open.
Most js-based tabbed layouts allow you to click an active tab to close
the card.  This seems reasonable to me, but it would require a slight
complexification of the card handling.

Should I expand the wording concerning activation and tab-selection to
allow other means of targetting (such as XPointer)?  I don't see any
particular reason *not* to.

I still liked that the original draft allowed you to easily transform
existing markup into a tabbed layout.  Can anyone see an easy way to
merge the two solutions?

~TJ

Received on Monday, 20 April 2009 14:44:48 UTC