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.

For a simple demo, see the menu of this site, op111.net.

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 or wayward browsers.

It is also a solution that suggested to me that the :not selector may offer possibilities that are unexplored. If you know of other useful applications of the :not selector, be kind enough to enligthen us with your comment!

Step 1

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

<span class="more"> &darr;</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:

<ul>
  <li>
    <a href="http://x.net/p1">Page1</a>
  </li>
  <li>
    <a href="http://x.net/p2">Page2</a>
    <ul>
      <li>
        <a href="http://x.net/p2/s1">Subpage1</a>
      </li>
    </ul>
  </li>
</ul>

... what we aim at is:

<ul>
  <li>
    <a href="http://x.net/p1">Page1<span class="more"> &darr;</span></a>
  </li>
  <li>
    <a href="http://x.net/p2">Page2<span class="more"> &darr;</span></a>
    <ul>
      <li>
        <a href="http://x.net/p2/s1">Subpage1<span class="more"> &darr;</span></a>
      </li>
    </ul>
  </li>
</ul>

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

Let’s see the second step now...

Step 2

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

li a .more { display: none; }

Why did we do that?

Because the indicators we added in the first step appear in all items, but not all items have subitems. So, in the second step we hid the indicators, and in the third 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; }

Done!

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 that 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 common browsers. So, all browsers will hide the indicators from display. Then, browsers that understand :not and :last-child will show the indicators where appropriate.

Simple, perfectly valid CSS 3, and, for this reason, future-proof too!

Fine... But!...

I want the helpful 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).

So, for Internet Explorer you will have to resort to a mixture of CSS and JavaScript. Either use something like Superfish or write some custom JavaScript. If the site already uses a JS framework, I imagine the code would be pretty simple. In jQuery, for example, all it takes is one line:

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

Then, load the JavaScript snippet conditionally if the user agent is Internet Explorer, so that good browsers don’t have to download unnecessary bytes.

I want different indicators for parents of different levels!

I omitted that from the example on purpose, to keep the markup as simple as possible. It’s easy to do:

  1. Add two indicators to each anchor, one for subitems that expand vertically and another for subitems that expand horizontally, and wrap each indicator in its own span along with an appropriate class. E.g.: more-v and more-h.
  2. Hide all indicators.
  3. For top-level parents display the one indicator by making a selector that applies to top-level parents only. E.g.:
    • #menu > ul > a:not(:last-child) .more-v
  4. For lower-level parents display the other indicator by making a selector that targets lower-level parents. E.g.:
    • ul ul a:not(:last-child) .more-h

Links

That’s it for now. Thanks for reading!

Here are a few links to help your work and research on drop-down menu systems:

Appendix

Here are three bits of code that show how to generate the markup for the indicators in WordPress.

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

wp_page_menu(array(
    'depth' => 2,
    'link_after' => '<span class="more"> &darr;</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"> &darr;</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"> &darr;</span>' . '</a>', $html);
}
add_filter('wp_list_pages', 'my_list_pages');

Comments (2)

  1. chuck says:

    I am confused about where to add the markup.

    wp_page_menu(array(
        'depth' => 2,
        'link_after' => '<span class="more"> &darr;</span>',
        'sort_column' => 'menu_order',
        'title_li' => '',
    ));
  2. demetris says:

    @chuck: What theme do you use? I ask because, before explaining how to add the markup, it would be useful to know whether the theme uses wp_page_menu or wp_list_pages.

Write a comment

Your email address will not be published. Required fields are marked *