Progressive enhancement of drop-down menus with the :not CSS selector

You probably have noticed the characters—usually arrows—displayed on some navigation menus to indicate the presence of subitems under an item. I find them helpful, and I recently looked to see if it was possible to have them displayed on a CSS drop-down menu by using only CSS (that is, without any JavaScript assistance). In this howto I explain the solution I arrived at:

How to display these useful indicators on a CSS drop-down menu by using the selector :not.

It is a simple solution that works in all browsers except Internet Explorer, where the indicators simply do not appear at all. Hence, it is a progressive enhancement, or, to put it the other way round, it degrades gracefully in old and wayward browsers.

Step 1

To each anchor in the menu we add an indicator-character wrapped in a span with an appropriate class:

<span class="more"> ↓</span>

For this howto I use the downward arrow as indicator, and more as the class name.

So, supposing we have a menu with two top-level items, one of which has a subitem:

    <a href="page-1">Page 1</a>
    <a href="page-2">Page 2</a>
        <a href="page-2/subpage-1">Subpage 1</a>

… what we aim at is:

    <a href="page-1">Page 1<span class="more"> ↓</span></a>
    <a href="page-2">Page 2<span class="more"> ↓</span></a>
        <a href="page-2/subpage-1">Subpage 1<span class="more"> ↓</span></a>

The best way to add the indicators will depend on how the markup is generated. If you use WordPress, look at the Appendix at the end for some examples.

Step 2

We make a CSS rule to hide all indicators we just added:

li a .more { display: none; }

Why did we do that? Because the indicators we added appear in all items but not all items have subitems. So, in the second step we hid the indicators and in the third and final step we will instruct the browser to display indicators only in items with subitems:

Step 3

We make a second CSS rule:

li a:not(:last-child) .more { display: inline; }

And that’s it.

How does this work?

In items with subitems, the a anchor is followed by a ul element (which containts the subitems). So, in items with subitems the anchor is not the last child of its li parent. This is the condition our :not rule specifies, saying, in human language:

Select .more within any anchor that is not the last child of its parent, and display it inline.

The display: none declaration of the second step is understood by all browsers. So, all browsers will hide the indicators. Then, browsers that understand :not and :last-child will show the indicators where appropriate.

It is a simple solution, perfectly valid CSS 3, and, for this reason, future-proof too!

Indicators in Internet Explorer too

That’s not possible with CSS alone, because IE (even IE8) knows nothing about CSS3 selectors like :not and :last-child (or :only-child, which would have the same effect here). It is possible, however, with a bit of JavaScript. With jQuery, for example, all it takes is one line:

jQuery(document).ready(function($) {
    $('a:not(:last-child) .more').show();

Different indicators for different menu levels

I omitted that from the example on purpose, to keep the markup as simple as possible. Here is how to do it:

Add two indicators to each anchor, one for subitems that expand vertically and another one for subitems that expand horizontally, and wrap each indicator in its own span with its own class, e.g. more-v and more-h.

Now hide all indicators.

Then, for top-level parents display the one indicator by making a selector that applies to top-level parents only. Something like:

#menu > ul > a:not(:last-child) .more-v

Finally, for lower-level parents display the other indicator by making a selector that targets lower-level parents. Something like:

ul ul a:not(:last-child) .more-h


A few links to help your work and research on drop-down menu systems:


Here are three examples of how to generate the markup for the indicators in WordPress.

If you are making a WordPress theme from scratch, use the link_after argument of wp_page_menu(). If the menu is generated by wp_list_pages(), you can pass the same argument to it too. Here is an example that uses wp_page_menu():

    'depth' => 2,
    'link_after' => '<span class="more"> ↓</span>',
    'sort_column' => 'menu_order',
    'title_li' => '',

wp_page_menu() and wp_list_pages() can also be modified by applying a filter, which is convenient if you do not want to edit template files directly.

For wp_page_menu() the indicator markup is added by applying a filter to wp_page_menu_args():

function my_page_menu($args) {
    $args['link_after'] = '<span class="more"> ↓</span>';
    return $args;
add_filter('wp_page_menu_args', 'my_page_menu');

For wp_list_pages() your filter will have to do a simple string replacement instead:

function my_list_pages($html) {
    return str_replace('</a>', '<span class="more"> ↓</span>' . '</a>', $html);
add_filter('wp_list_pages', 'my_list_pages');