A List Apart

Menu

CSS Swag: Multi-Column Lists

Issue № 204

CSS Swag: Multi-Column Lists

by Published in CSS, HTML, Layout & Grids · 59 Comments

One of the minor holy grails of XHTML and CSS is to produce a single, semantically logical ordered list that wraps into vertical columns.

Article Continues Below

The ideal situation, in my view, would be a single XHTML list whose wrapping is controlled entirely by CSS. Further, the wrapped list should tolerate text resizing (easily accomplished by styling everything in ems).

CSS and the browsers that support it don’t yet provide us with “vertical wrap,” so we have to augment basic list markup with additional attributes and styling rules to achieve the effect. I’ll take you there—or as close as we can get using today’s browsers—but along the way let’s look at a variety of ways to accomplish a similar effect.

We’ll be shooting for something that looks a bit like this:

An ordered list styled in vertical columns

(Why, you’ve doubtless been pondering, is this article entitled “CSS Swag”?  In hunting for a metaphor for the multi-column list styling shown in the above screenshot, I pictured a strip of cloth that hung down, drew back up to its initial height, then draped again…  A quick google led me to this wonderful term, which I had previously associated only with an Australian tramp’s bundle of possessions, and to a great etymology that links swagger to sway.)

Watch your step

I’ll warn you up front. If you want to present a list in multiple columns, you’ll have to compromise. You can sacrifice W3C web standards and use deprecated markup, you can live with markup that’s less than semantically logical, you can tolerate a mixture of presentation with content, you can say goodbye to browser compatibility, or you can use markup that’s heavy with attributes and styling that’s heavy with rules. Every road exacts a toll.

While each of these methods is simple enough to set up, the acid test comes when you add or remove list items in the course of website maintenance. How easy to modify is your chosen method? Ideally, we should be able to simply insert or delete list items and watch the list rewrap itself.

The reality is not so ideal. Unlike the horizontal wrap that our browsers handle automatically, vertical wrapping requires us to explicitly state which list items occur in which columns or where the columns should break. To keep a modified list wrapping properly, we must rearrange the list items, change classes and attributes in the markup, or tweak rules in the stylesheet.  The only technique described here that doesn’t need any of that fuss (Method 1) has other serious behavioral problems.  Finally, because vertical proportions are so important here, most of these lists are going to break if we assume that each list item will occupy one line, only to have some items wrap to two or more lines.

So why do we bother?  Well, because the final effect is so cool—and practical.  Wrapping a list into columns can relieve the website visitor of the necessity to scroll down a long list.  A three- or four-column list can fill the width of a page while a single skinny column could leave the layout looking anemic.  There are as many reasons to wrap lists as there are web designers:  very definitely More Than A Few.

To work, then.

First, expunge all white space

The default rendering of an XHTML ordered list in browsers is that of a single vertical series of items:

  <ol>
  <li>Aloe</li>
  <li>Bergamot</li>
  <li>Calendula</li>
  </ol>

Default rendering of ordered lists

To bypass some browser inconsistencies, I’ve taken to marking up my lists like this:

  <ol
    ><li>Aloe<li>Bergamot</li
    ><li>Calendula</li
  ></ol>

I’ve moved the last closing angle-bracket > of each row to the beginning of the next row.  This keeps each list item on a row by itself for the benefit of human readers and at the same time effectively eliminates all of the white space between tags, this producing more consistent rendering across browsers.

(My pet theory as to why Internet Explorer includes the white space between list-item tags in its rendering calculations is that it’s a hold-over from that prehistoric era when list items, like table cells, didn’t have closing tags.  Back then, a browser properly paid attention to all text including white-space from one start-tag <li> to the next.  When closing tags were added to the mix, apparently no one at Microsoft remembered—or deemed it important enough—to adjust the logic to stop parsing between one closing tag </li> and the next start-tag <li>.)

Also, I’m adding a hyperlink to each list item.  This allows me to check for abnormalities in rendering anchors when various CSS rules are applied:

    ><li><a href="#">Aloe</a></li

Note: most of my examples use ordered lists because item sequence is paramount in this exercise; unordered lists may, of course, be substituted.

And one more thing: the example pages attached to this article are all marked up with a strict doctype. I use this in all my web work these days because it appears that I can get more consistent results cross-browser with less weeping and gnashing of teeth.  If you use a transitional doctype or run in quirks mode, you may need to tweak the CSS to get these methods to behave cross-browser.

Method 1: Floating list items

Of all of the techniques I’m going to describe, this one uses the cleanest XHTML markup and some of the simplest CSS.  Unfortunately, its flaws prevent it from becoming my method of choice.

Method 1: Floating list items

See Example 1.

The technique is simple: give the list items a fixed width and float them left.

The list items wrap horizontally like words in a paragraph.  Generally speaking, when a series of blocks are floated left or right, they align horizontally and wrap around when they reach the maximum width of their container.  If just three items can fit on one row, as in this example, the list naturally wraps into rows of three columns.

The XHTML markup is a straightforward list with no special classes or other attributes required.  To prevent subsequent page elements from being affected by the float I’ve contained the list in a <div> and cleared the float with a (non-semantic) <br />:

<div class="wrapper">
  <ol
    ><li><a href="#">Aloe</a></li
    ><li><a href="#">Bergamot</a></li
    ...
    ><li><a href="#">Oregano</a></li
    ><li><a href="#">Pennyroyal</a></li
  ></ol>
  <br />
</div><!-- .wrapper -->

The essential CSS is brief:

  /* allow room for 3 columns */
  ol
  {
    width: 30em;
  }  /* float & allow room for the widest item */
  ol li
  {
    float: left;
    width: 10em;
  }  /* stop the float */
  br
  {
    clear: left;
  }  /* separate the list from subsequent markup */
  div.wrapper
  {
    margin-bottom: 1em;
  }

How to edit: items can be added or removed with no further changes to the XHTML or CSS.

This method fits two of our criteria for an ideal solution. The list itself is a simple, single XHTML list and the column-wrapping is controlled entirely from the stylesheet. However, it falls short of our goal because the list sequence runs across and then down instead of descending in vertical columns.  Also, it doesn’t survive well cross-browser: Internet Explorer and Opera suppress the item markers (ol numbers and ul bullets) when list items are floated left or right.

Method 2: Numbering split lists with HTML attributes

This tactic—perhaps born of a desperate desire to tame web design to be as obedient as print design—is to split the list into multiple sub-lists and arrange them side by side.

Method 2: Numbering split lists in XHTML

See Example 2.

This approach has several flaws: the semantic integrity of the single list is sacrificed; list wrap, which I consider presentational, is determined entirely by the HTML markup rather than by the stylesheet; and item numbering resets to “1” with each new list.

That last hurdle, at least, can be leapt over if you don’t mind using deprecated markup.  (In order for this markup to validate, you’ll need to use a transitional doctype.)

As Example 2 demonstrates, the now-deprecated HTML attributes start and value let us reset list numbering.  If we ignore the W3C recommendation and use deprecated markup, we can mark up separate lists to give the illusion of one continuous sequence.

Here’s an example I’ve pared down to highlight the attribute placement:

  <ol
    ><li>item</li
    ><li>item</li
  ></ol>  <ol start="3" 
    ><li>item</li
    ><li>item</li
  ></ol>  <ol
    ><li value="5">item</li
    ><li>item</li
    ><li>item</li
  ></ol>

These list items will be numbered sequentially 1–6 by browsers and other user agents that still support the deprecated markup.

The stylesheet’s only job, then, is to float the sub-lists side by side, then clear the float after the final column.  (In them olden days before we got religion, sub-lists were commonly positioned inside adjacent table cells.)

How to edit: When list items are added or deleted, some items will need to be moved from one sub-list to another to maintain a consistent column length.  If the number of items per column changes, the start and value attribute values in XHTML will need to be tweaked to maintain proper numbering.  No CSS changes are required.

Method 3: Numbering split lists with CSS generated content

If you’re splitting your list into chunks, another tool for numbering the items contiguously is CSS content generation, wherein a stylesheet causes text to appear that doesn’t exist in the XHTML markup.

Method 3: Numbering split lists with CSS

See Example 3.

List item numbering and bulleting are actually two instances of content-generation, but they’re the only ones that are supported universally, probably because they date back to early HTML before CSS was born.  The other forms of CSS content-generation are ill-supported in today’s browsers: of those tested, only Opera rendered this example properly.

The XHTML markup is similar to that in Method 2 but even cleaner: the list is divided into two or more sub-lists, but the start and value attributes are omitted.

Using CSS we suppress normal list item numbering, then apply the pseudo-element :before to insert incremental values:

  ol li
  {
    list-style-type: none;
  }  ol li:before
  {
    content: counter(item) ". ";
    counter-increment: item;
  }

Sharp eyes will have caught one subtle difference in formatting in the screenshot above: the numbers generated with :before are rendered flush-left, whereas browsers typically align ordered list numbering flush-right.  This affects the alignment of the item values when the number of digits changes, as in 9 to 10 above.

How to edit: When items are added to or removed from the overall list, some items will need to be moved from one sub-list to another in XHTML to maintain a consistent column length.  No further change is necessary to either XHTML or CSS to preserve proper numbering.

Unfortunately, this technique will not be practical for any cross-browser purpose until more browsers support the :before pseudo-element.  For now, Opera’s the only browser that can render it.

Numbering split lists with script

While we’re on the subject of content-generation, I’ll add in passing that the items in split lists can also be “physically” numbered contiguously by a script that inserts numbers into the item values, either server-side during page generation or client-side upon page-load.

A server-side script such as ASP or PHP, reading list item values from a database or a flat file, can prepend a consecutive number to each item as it’s written to the page.  It can also divide the total number of list items by the desired number of columns and generate the sub-list markup.  The task of arranging the sub-lists side by side can be left to a static stylesheet.

A client-side script such as JavaScript can locate the sub-lists using ids or classes and iterate through the items, inserting item numbers, suppressing normal ordered list numbering, and splitting one long list into sub-lists as needed.

It should go without saying (but doesn’t) that any solution that uses client-side scripting should gracefully degrade in browsers with scripting turned off.

Wrapping a single list

As we’ve seen, splitting a list into sub-lists and forcing consecutive item numbering can produce the desired cosmetic effect, but those solutions aren’t as satisfying to me as one that preserves the integrity of the single list markup.

To wrap a single list into columns, I use CSS to grab each item that begins a new column and drag it back up to the level of the first item and then over to a new left margin.  Normal rendering does the rest.

Now we’re talkin’ swag!

Here are three ways to mark this up:

Method 4: Wrapping a single list with XHTML

If you’re willing to control where the column-wrap occurs using the XHTML markup, it’s easy enough to mark each item according to which column it belongs to.  An additional class name marks the first item of each column.

Method 4: Wrapping a single list with XHTML

See Example 4.

Here’s an abbreviated example:

  <ol
    ><li class="column1">item</li
    ><li class="column1">item</li    »
    ><li class="column2 reset">item</li
    ><li class="column2">item</li    »
    ><li class="column3 reset">item</li
    ><li class="column3">item</li
  ></ol>

The stylesheet uses these classes to establish the horizontal column positions:

  li.column1 { margin-left: 0em; }
  li.column2 { margin-left: 10em; }
  li.column3 { margin-left: 20em; }

Then we mandate the line-height of each item and bring the first item of each column back up to the level of the first item:

  li
  {
    line-height: 1.2em;
  }  li.reset
  {
    margin-top: -6em;
  }

Vertical return = number of items in a column ∗ height of each item.  In this case, 5 items ∗ 1.2em = 6em.  (When I’ve tried making the line-height smaller than 1.2em I’ve run into cross-browser discrepancies.)

How to edit: When items are added to or removed from the list, the XHTML markup must be tweaked to give items the proper column-classes, and the reset class must be moved to the first item of each column.  When the number of items per column changes, the CSS li.reset{} rule must be changed accordingly.

This isn’t my preferred technique because the column-wrapping is controlled from the XHTML markup rather than the stylesheet.  The way I look at it, wrapping a list into vertical columns is a matter of presentation, not content, and therefore ought to be controlled by the stylesheet in the interests of separating content from presentation.

Wrapping a single list with CSS

In Methods five and six that follow, the points at which the list wraps to a new column are controlled wholly from the stylesheet.

The price we pay is some heavy XHTML markup, gravid with class names.  Check this out:

We prepare the markup by assigning a unique class to each list item:

  <ol
    ><li class="aloe">Aloe</a></li
    ><li class="berg">Bergamot</a></li
    ><li class="cale">Calendula</a></li
    ...
    ><li class="oreg">Oregano</a></li
    ><li class="penn">Pennyroyal</a></li
  ></ol>

(I’m using class instead of id here so that I’ll have the freedom to include more than one wrapped list on the same page; a class may apply to more than one object, but an id must be unique.  The important thing is that each item is uniquely identified within its list.)

In the stylesheet, we assign a different left margin to each group of items that belong in one column:

  li.aloe,
  li.berg,
  li.cale,
  li.dami,
  li.elde
  {
    margin-left: 0em;
  }  li.feve,
  li.ging,
  li.hops,
  li.iris,
  li.juni
  {
    margin-left: 10em;
  }  li.kava,
  li.lave,
  li.marj,
  li.nutm,
  li.oreg,
  li.penn
  {
    margin-left: 20em;
  }

This is reminiscent of the “columnN” classes in Method 4, however here the determination of which item belongs to which column is decided purely on the CSS side of the street.

Oh, the irony of it all!  The prospect of assigning a unique class to each list item is enough to make eyes cross and toes curl.  After all, the reason we’re using ordered lists in the first place is to take advantage of the automatic list numbering that our browsers provide.  If we have to name each list item class uniquely, why not just number the list items themselves and be done with it?

Easy, now.  Breathe.  The assignment of unique item classes isn’t about numbering the items, it’s about the presentation of the whole list—finding ways to persuade the browser to wrap it into columns without actually having to chop the list into pieces and paste them side by side.  In web design we assign classes and ids to page elements routinely to guide CSS presentation; that’s what they’re there for.  Make no mistake, assigning a different class to every item in a list is no one’s idea of elegant code, but it works, it validates, and it doesn’t (in my humble opinion) make the markup intrinsically ‘un-semantic.’

Whether it’s worth your while to manage the code for any of these techniques when you modify a list will depend on how much you want multiple-column lists to work.  Fortunately, it’s not that big a deal.  Editing small lists by hand is easy, and if you’ve got a really long list that’s constantly changing you should probably be generating it from a database in the first place.  One consolation is that when we’re generating pages from a server-side script we can assign unique list item classes automatically so we don’t have to get our fingers dirty.  (Or tired.)

Method 5: Wrapping a single list using absolute positioning

Because we’ve now identified each item uniquely within the list, one possible approach is simply to position each item explicitly.

Method 5: Wrapping a single list using absolute positioning

See Example 5.

Note that this is not a practical cross-browser solution today for ordered lists, since neither Internet Explorer 6 nor Opera 7 will display list markers when list items are styled {position: absolute;}.

To make this work for the rest of the browsers, the entire list must be enclosed within a div with {position: relative}: this gives the absolutely-positioned list items a frame of reference so we can prevent them from simply flying up to the top of the page.  Then it’s simply a matter of assigning vertical positioning to every element.  We can do this in rows:

  li
  {
    position: absolute;
  }  li.aloe, li.feve, li.kava { top: 0.0em; }
  li.berg, li.ging, li.lave { top: 1.2em; }
  li.cale, li.hops, li.marj { top: 2.4em; }
  li.dami, li.iris, li.nutm { top: 3.6em; }
  li.elde, li.juni, li.oreg { top: 4.8em; }
  li.penn                   { top: 6.0em; }

How to edit:  When the number of items per column changes, the stylesheet will need to be changed.  When items are added to or removed from the list, the stylesheet must be edited to re-determine which items reside in which columns.  Every new list item must be assigned a unique class in XHTML.

Absolutely positioning every item in a list is a control-freak’s dream, the sort of approach many of us took when we first came to the web from print design and hadn’t yet learned to let go.  To create multiple-column lists it isn’t necessary to position every item, as Method 6 will demonstrate, but I’m including it here for the sake of completeness.  It does ’break’ differently from Method 6 and that might be one basis for choosing it:

If any list item is long enough to wrap around to a second line, how does that affect the layout of the list?  When list items are absolutely positioned as in Method 5, the overall layout will remain unchanged, but the list item that wraps will be overlaid by the next item in the list which will claim its position without regard to preceding text, since absolute positioning takes each item out of the flow.  In contrast, in Method 6 below each column of list items descends by normal flow; a list item that wraps will push down subsequent items, lengthening the column it’s in.  Because that method assumes a fixed column height, the vertical return will then be insufficient to bring the next column back up to the top, creating a staggered layout.

There may be other ramifications of absolute positioning that will affect our choice: for example, some browsers don’t permit the user to highlight text in absolutely-positioned blocks.

Method 6: Wrapping a single list using normal flow

Finally, here’s the technique I prefer to use: a single semantically-logical list whose column-wrapping is controlled entirely from CSS, relies on normal flow, and works in most modern browsers.

Method 6: Wrapping a single list using normal flow

See Example 6.

As in the previous method, each list item is given a unique class name in XHTML, and the left margin of each column is stipulated in CSS.

What differentiates this method is that here we use those unique item classes to bring the first item of each column back up to the top using a negative margin-top:

  li
  {
    line-height: 1.2em;
  }  li.feve,
  li.kava
  {
    margin-top: -6em;
  }

Again, vertical return = number of items in a column ∗ height of each item.  In this case, 5 items ∗ 1.2em line-height = 6em.

How to edit: when items are added to or removed from the list, the uniqueness of item class names must be maintained in XHTML and the stylesheet must be tweaked to shift items to their proper columns.

Gettin’ fancy

With a little extra styling and some background images, the list can get ready to party without breaking the multi-column flow.

Example 7: Gettin’ fancy

See Example 7.

Where to now?

There are many ways to display a multiple-column list.  As we’ve seen, many of them require a compromise of web standards or browser-compatibility, as well as some pretty hairy markup and styling.  The best choice, in my opinion, is to give the XHTML markup sufficient “hooks” to allow the column-wrapping to be controlled entirely from CSS.

What we really need is for some bright bulb to come along and figure out how to do this with spare markup—and actually claim that holy grail.

Now, go forth and swag!

Browsers & helpers

The examples for this article look substantially the same (except where specifically noted in the text) in Windows browsers Firefox 1.0, Internet Explorer 6, Mozilla 1.7.2, Netscape 7.1 & 6.2, and Opera 7.23, and in Macintosh browsers Firefox 1.0, Internet Explorer 5.2, and Safari 1.0.3.

Less successful with these methods are Windows Internet Explorer 5.x and earlier, Linux Konqueror 3, and Netscape 4.x.  Perhaps with coaxing they could be persuaded to come along as well.

Thanks to Angela Marie, B.J. Novitski, Bruno Fassino, Ingo Chao, Larry Israel, and Zoe M. Gillenwater for their helpful criticism and browser peeks.

Angel Wing photograph in example seven by Sophie Arés Pilon.

About the Author

59 Reader Comments

Load Comments