Saturday, January 09, 2010

A CSS/jQuery solution for creating multi-column lists

I seem to have found a solution for one of those front-end developer "Holy Grail" challenges - getting an unordered list to rearrange itself into two (or more) columns if the content of the first column gets too long.

Before I explain how to do it, I should point out that the vast majority of this solution is already out there in the form of a neat jQuery plugin called Columnizer jQuery written by Adam Wulf. I had nothing to do with the creation of it - I just stumbled upon it when I was googling for a solution, and wondered if I could adapt it for my particular layout problem. I added a bit of additional jQuery so it would do exactly what I wanted, and to my complete astonishment - it worked. Amazing!

There are a bunch of CSS-only solutions for multi-column lists which kinda sorta work - as long as you have a static list and can attach classes to various individual <li> tags. They generally work on the principle of moving some of the list items to a different position on the page using CSS - so that the list remains intact within the HTML, but appears to be broken up into multiple columns when viewed in your browser.

The CSS Swag: Multi-Column Lists article at A List Apart is one of the best examples of this, and in fact shows a whole bunch of different ways of achieving this goal.

However (and it's a big "however") - none of these solutions will work with a dynamically-generated list where you don't know from page to page how many items will be in that list, and where you can't add your own individual classes or IDs to each <li> in advance.

A perfect example of this situation - and the one I was wrestling with - is when you're building a set of templates which will be integrated into a CMS, and the unordered list in question is the subnav - which has to fit into a fixed-height space.

Here's what the subnav in my design normally looks like:

Single-column subnav list.
But... I do not know how many pages the client will end up creating within each subsection of their new website. What I do know is that the number of list items in each subnav menu will vary from section to section, and will be generated automatically by the CMS (which means I can't add classes or IDs).

So this is what I want it to do when the number of items in the list gets too long for the fixed-height space to contain them all:

2-column subnav list.
One thing to note: you need to be sure that your client isn't going to use massively long names for their pages, as these will generally translate into massively long links in the subnav, in which case you will soon run out of room - especially in the 2-column layout. You can pre-empt this by training them to use short page names and/or re-name them for the subnav. Most CMSs will let you do this - Silverstripe, for example, which is what we're using for this site - has an additional field in each page where you can define the text you want to be used as the subnav link. Very sensible.

Adam Wulf's plugin is designed to automatically lay out your content in newspaper column format. You can specify either column width and/or height or a static number of columns. I noticed that one of the lines of code in his jQuery was

...which made me think he might have included something to ensure that lists which get split into two different columns still work properly - and he has! Clever man. We're 90% of the way there already!

I have nowhere near the technical expertise required to write my own jQuery plugins - or even to understand all the code within a plugin - but I do have enough of an understanding of jQuery to be able to utilise what others have created and adapt it to my own requirements - sometimes.

I'm not even going to try and explain how Adam's plugin works - because I don't actually know - I'm simply going to highlight the bits you need to make this list do its thing, and then show you the bit of jQuery I added which makes the list do one thing when it's short - and another when it's long.

Here's how I did it...

I began with Adam's Sample 5 page. He describes this example as one that:
Shows fixed width and height columns scrolling horizontally
...which seemed to be what I was looking for.

I viewed the source of his example page and created a copy for myself - test-columnizer. Then I replaced his example text with a simple unordered list, exactly like the subnav code I'm going to be using. You can see my test example here - test-columnizer2. Because his columns were initially set to be 400px high I put a whole bunch of list items in my test list to make sure it was working properly.

In the jQuery function I removed
because I didn't need it, and replaced it with
The class "dontsplit" is built into the plugin and prevents individual list items from being split into two columns. The jQuery now looks like this:
width : 300,
height : 400
I also removed the <div class="thin"> </div> in the HTML (you can see it towards the end of the page in test-columnizer) because, again, I don't need it. This "thin" div in the plugin can be set to contain any overflow from columns created within the "wide" div - you can see an example on Adam's website - Sample 4 page. Because I don't intend to have any overflow in this design, it can come out.

The next step was to style the list so that it looked more like my design, and to place it inside my subnav div, which is a fixed width, fixed height container. You can see an example here - test-columnizer3.

I altered the jQuery to this:
width : 90,
height : 150
...which tells the plugin to make each column 90px wide by 150px high.

In addition to my specific CSS styling for the subnav box and the list items, you'll also notice I made a couple of changes to Adam's existing CSS styling. I set a specific width on .wide, like this:
.wide { 
clear: both;
width: 190px !important;
Without this width setting, the total width of .wide is calculated by the plugin, and ends up as 180px (2 columns at 90px each = 180px). I wanted a gutter of 10px between the two columns, which meant that the .wide div needed to be 190px wide in total. By adding !important to the style in the CSS I can force it to override the plugin's calculated width.

Adam has very helpfully coded the plugin to add a class of "last" to the last column in the div, which meant I could then add this style to my CSS:
#subnav .wide .last {
float: right !important;
} force the last column to float right - so creating that gutter of 10px between the first (left-hand) column and the last (right-hand) column. Again, by adding !important I can force it to override the plugin's default float: left.

Here's the example page again - test-columnizer3 - in case you missed the link earlier.

OK - so now let's look at what the subnav does if the number of items in the list is quite small - test-columnizer4.

Hmmm. That's not exactly what I want. If there are just a few list items, and we don't actually need two columns to fit them all in, I want a single column that stretches right across the subnav space - like this:

Single-column subnav list.
...not like this:

Doh! Single-narrow-column subnav list.

In other words I need an if... statement in the jQuery that will trigger the columnize plugin only when the number of list items exceeds the space available in the first column.

Firstly I defined a variable maxHeight of 150px, and then created my if... statement which basically says "if the height of the .wide div is greater than 150px, then run the jQuery plugin." The whole thing looks like this:
var maxHeight = 150;

if ($('.wide').height() > maxHeight)
width : 90,
height : 150
I decided to set my variable as a maximum height rather than by counting the actual number of items and setting a maximum number of items in each column because a) individual list items might sometimes wrap onto more than one line - which would mess up the calculation and b) if you're viewing the website in an older browser like IE6 or IE7 and you've increased the font size, it will again mess up the calculation (although I guess I could have overcome that issue by using ems).

You can see an example of the completed solution here: test-columnizer5.

And here's the same solution with a few more items in the list to show how it rearranges itself into 2 columns when necessary: test-columnizer6.

Perfect! Now the subnav list spreads nicely across the whole div, unless there are too many list items to fit into the fixed-height space, in which case it splits into two separate lists in two columns, with each column only half the width of the original single column.

I've tested the solution in IE6, IE7, IE8, Firefox, Opera and Safari on both PC and Mac where appropriate, and it works just fine. Incredible.

Thanks a million to Adam Wulf for coming up with an awesome plugin that allowed me to do exactly what I wanted with just a little tweak at the very end. It's a great piece of work!

I hope that by blogging about it I'll flag it as a solution for the multi-column list problem, which will make it easier to find for those googling for a fix. Let me know if it works for you.

Technorati tags: , , , , , , , , , , , , , , , , , .


Afonso said...

u sav'd my life xDD

u're a genius

Anonymous said...

Thank you - it's amazing!!!