Sunday, April 19, 2009

Styling first and last list items (and Wordpress)

Sometimes a design calls for the first and last items in a list to be styled differently for the others, for example a line or column of overlapping tabs.

For most modern browsers this is trivial and can be done using the list-class:first-child and list-class:last-child psuedoclasses. It's just that Internet Explorer doesn't support these very well.

If you are hardcoding the list or writing the generation routine it is also simple to generate 'first-item' and 'last-item' classes in the appropriate place.

<ul id="list-id">
<li class="ie-list-first list-type-item">Stuff</li>
<li class="list-type-item">Stuff</li>

<li class="list-type-item">Stuff</li>
<li class="ie-list-last-last list-type-item">Stuff</li>

When templating for an existing CMS it may not be desirable to dig deeply enough to customise the code. For example WordPress's wp_list_pages() function (with appropriate parameters) returns something like this:
<li class="page_item page-item-4 current_page_item">Page link</li>
<li class="page_item page-item-9">Page link</li>

<li class="page_item page-item-13">Page link</li>
<li class="page_item page-item-15">Page link</li>
<li class="page_item page-item-34">Page link></li>
<li class="page_item page-item-25">Page link</li>

I spent some time following the function trail before deciding it was better to add the classes after the string was generated. Fortunately if you set 'echo=0' wp_list_pages() will return the string to a variable. After that it's just a matter of string substitution - first and last incidence of class=" . (This might get a bit trickier if you're using nested lists, but I try and keep depth=1 if possible).

It turned out the easiest way to do this was with a preg_replace - but it still requires a little trickery because you can't tell preg_replace to start from the end of a string. Instead you have to reverse everything, do the replacement, and then reverse everything again.
//get li string
$pages = wp_list_pages('title_li=&depth=1&echo=0' );

//add styling identifiers for first item
$pages = preg_replace('/class="/','class="ie-list-first ', $pages, 1); //note space on end of replacement string

//add styling identifiers for last item
$reversedString = strrev($pages);
$reversedSearch = '/'.strrev('class="').'/';
$endClass = strrev('class="ie-list-last '); //note space on end

$pages = strrev(preg_replace($reversedSearch,$endClass, $reversedString, 1));

echo $pages;

I make sure the ie-list class is first because it makes it easier to call in your IE-specific stylesheet without worrying that another CMS generated class will get precedence.


sean.afk said...

I think you're missing a double quote:

$reversedSearch = '/'.strrev('class="').'/';

Robert said...

By jove, I think you might be right. Edited to fix, thank you very much :)

Simon said...

Hi Robert

This piece of code was just what I was looking for to style the first and last elements of my wordpress menu but I'm getting an error on this line:

$endClass = strrev('class='"ie-list-last '); //note space on end

Everything after that in the editor is not showing the correct syntax highlighting which indicates that something is not quite right and the browser is throwing a "Parse error" at that line in the code. If I comment it out everything works fine but no last item class... any idea's/help would be greatly appreciated :-)


Robert said...

Hi Simon,

you're right, a close inspection shows I've managed to stick an extra single quote in that line right next to the double quote.

replace it with

$endClass = strrev('class="ie-list-last '); ; //note space on end

[fixes post]

Simon said...

Thanks Robert, that fixed the error :-) but now I'm getting a different issue... the class being applied to the last list item is ending up like this:

<li class="last " page-item-13="" page_item="">

page-item-13 and page-item are the existing classes but it looks like the quotes are still out of sync.

Also, a question: what would I need to change (or is it even viable) to add the first and last class to the a tag withing the list element rather than the list element itself?

Cheers :-)

Robert said...

Hi Simon,

I think the problem is now with this line:

$reversedSearch = '/'.strrev('class=').'/';

should be

$reversedSearch = '/'.strrev('class="').'/';

If not I'll have to go dig up the project I was working on at the time and see what I was actually doing. Obviously I did not manage to copy the code perfectly into blogger :-/

It's feasible to change the <a> tag as long as you can identify a constant string around the class to work with

- if it doesn't already have a class changing out the search string for /<a/ and the replace string for /<a class="[class-name]"/ ought to do it

- I'd leave it as is and apply css as eg:
li.first a:link, li.first a:visited {[style]} myself

Simon said...

You're a legend! Thank you so much :-)

I had to make a couple tweeks to get the link class working but it now works a treat! The reason I want the class on the a tag rather than the list item is so that I can style the linked based on the currently selected item:

.current-page-item { default list item style }
.current-page-item .first { first list item style }
.current-page-item .last{ last list item style }

this way I can add menu ends that are current page aware. :-)

I'll flick you a link to the finished site when it rolls if you like.

Robert said...

Glad to be of service :) Please do send over the link when you are done.

You know you can chain-reference classnames within the same class attribute by omitting the space between them? So for example

<li class="current-page-item"><a class="first">
.current-page-item .first

is equivalent to

<li class="current-page-item first"><a>
.current-page-item.first a

This is more useful when you have multiple elements to style within your first or last item :)

Simon said...

Wow... I didn't know that, very handy! How browser safe is chaining though? I know we all hate IE6 but it's often a pain we have to bear ;-)

Another option that came to mind was utilising jQuery's :first-child and :last-child selectors.

Robert said...

Chaining is IE6 safe in my experience.

jQuery and other javascript solutions analyse the DOM to get around IE's lack of support for :first-child, :last-child (and now :nth-child). I prefer to use css if possible because more people have javascript disabled than css, and more non-visual - actually make that non-PC (eg mobile) - browsers are css-aware than javascript aware.

stein™ said...

Great tip!

Is it possible to use this on list_categories?

I have this string;

but i can't get it to add to the class names.

Robert said...

Hi stein™

as with wp_list_pages you need to tell wp_list categories to return the html as a string for further processing rather than echoing it to the browser directly. Use 'echo=0' for this.

$categories =

See for a list of all the available parameters.

Robert said...

Oops. Missed the concatenation.


John said...


xSEOn said...

The perfect solution! Thank you Robert! After hours on trying different plugins and other bul..ts your simple script gave me exactly what I needed to create a working multilingual overlapping tabbed menu in half an hour.

Randy said...

I needed to remove the border-right off my last list item in the footer, and I could not for the life of me figure out why I was having trouble manipulating the returned value of the wp_list_pages() function. I tried assigning it to a variable so I could search the $output string with other functions, I tried while/if statements, I tried CSS :last-child and adjacent selectors... nothing. Before I read your solution I did try using the echo=0 argument a few times but the Codex doesn't make it clear enough what it actually does (prevents automatic echo of $output), so I stopped using it until I read this. Thank you for this solution, this problem kept me up for many hours!

squidz said...

I too have been trying to conquer this little lingering item. I can see that the code here works. However, I don't quite get how to make it apply to my existing pages list menu. When I put the code in my functions.php file, it the creates its own list at the very top of my page. My normal menu remains with the styles unapplied.

What am I missing here?

Robert said...

Hi Squidz,

You need to put at least the line

echo $pages;

in your template in place of the current page generation function (wherever it calls wp_list_pages() normally, that's header.php in the site I'm looking at right now).

I personally put the entire chunk of code at that location to make the transformation obvious.

ben said...

great bit of code cheers :)