Targeting Menu Elements with Submenus in a Navigation Bar

Guest Author //

The following is a guest post by Ray Messina. Ray was interested in sharing this technique as a way to pay forward things he's learned from this site in the past, which is awesome. You might be aware of the jQuery .has method, which allows you to select an element if it has any of another selector as a descendant. There is sadly no such selector in CSS yet. But if you know a little something about the HTML structure, you can use a combination of positional selectors to mimic it. Ray will explain.

Recently, I was working on a dropdown navigation bar and wanted to differentiate the menu items which contained sub menus from those that didn't. I wanted to be able to do this automatically, without relying on JavaScript or having to add classes to the markup. I just wanted it to work as if the item just knew it had a sub menu or not.

The Need for Navigational Hints

As you probably know, menus are lists of links and, as such, it is standard practice to mark them up as <ul>s. By extension, drop down menus are merely nested <ul>s. Dropdowns are a common component of modern (and not so modern) web design. Using pure CSS, one can style the upper level of a navigation menu any which way, and hide the sub levels so that they are revealed only when the visitor hovers on the appropriate area.

Many designers are satisfied with leaving as that. However, from a UX/UI point of view, this is lacking as it leaves the user having to explore the entire menu to find which sections contain additional navigation links. This means visitors will either waste their time looking and get frustrated for doing so or miss areas of your site all together! That's no good at all.

The common way to address this issue, is to simply add a class to the <li>s that contain <ul>s (sub menus) so that we can style those items differently from those that do not contain sub navigation. Pretty simple, but also pretty tedious and not particularly graceful.

Another way, assuming that upper level items are not coded as links, is to use that difference in tags as leverage. That is, create faux links: style <span>s that's are direct children of the <li> to indicate that there are more links that follow and, by contrast, style anchor tags as not having additional sub items. This is handy if your drop-down is simple and you can structure it specifically that way. It's cleaner in that you don't have to add "submenu" classes to your markup but, just as before, it still requires that you structure the content of your list manually or that your CMS can foretell which list items will contain other <ul>s as sub menus.

Doing It Automatically!

I had come across other methods of automatically styling list items which contain other lists, but they employed absolute positioning and a pseudo element off the child <ul>. While clever, the CSS calculations can be tricky at times or the method may be entirely impossible to implement depending on what other techniques you have used to lay out your menu and/or position your submenus, or the overall effect you desired to achieve.

A better way

Ideally, it would be great if there was a CSS selector that would let us query if an element contained another kind of element as a direct child, something akin to jQuery's .has() method.

We can can achieve nearly the same thing with li a:first-child:nth-last-child(x) { }.

The key is having an expected child count (HTML element planned parenthood?). Most likely it will be two elements: the anchor and the <ul>, though one can tweak this technique to work for any number or child elements, as long as you have a regular pattern.

Here is a quick example. The markup is just your standard nested UL, but note that I have only used one class in the HTML, on the root <ul>. Test this out yourself, add any number of nested lists at any level!

Creating a Dropdown Demo

Let's put this idea to the test!

The HTML: Keep It Clean

<nav>
  <ul class="nav">
    <li><a href="#">About</a></li>
    <li><a href="#">Portfolio</a>
      <ul>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
      </ul>
    </li>
    <li><a href="#">Resume</a>
      <ul>
        <li><a href="#">item a lonng submenu</a></li>
        <li><a href="#">item</a>
          <ul>
            <li><a href="#">Ray</a></li>
            <li><a href="#">Veronica</a></li>
            <li><a href="#">Bushy</a></li>
            <li><a href="#">Havoc</a></li>
          </ul>
        </li>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
      </ul>
    </li>
    <li><a href="#">Download</a></li>
    <li><a href="#">Rants</a>
      <ul>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
        <li><a href="#">item</a></li>
      </ul>
    </li>
    <li><a href="#">Contact</a></li>
  </ul>
</nav>

The CSS

Some general styling to bring in the sexy:

nav {    
  display: block;
  text-align: center;
}
nav ul {
  margin: 0;
  padding:0;
  list-style: none;
}
.nav a {
  display:block; 
  background: #111; 
  color: #fff; 
  text-decoration: none;
  padding: 0.8em 1.8em;
  text-transform: uppercase;
  font-size: 80%;
  letter-spacing: 2px;
  text-shadow: 0 -1px 0 #000;
  position: relative;
}
.nav{  
  vertical-align: top; 
  display: inline-block;
  box-shadow: 
    1px -1px -1px 1px #000, 
    -1px 1px -1px 1px #fff, 
    0 0 6px 3px #fff;
  border-radius:6px;
}
.nav li {
  position: relative;
}
.nav > li { 
  float: left; 
  border-bottom: 4px #aaa solid; 
  margin-right: 1px; 
} 
.nav > li > a { 
  margin-bottom: 1px;
  box-shadow: inset 0 2em .33em -0.5em #555; 
}
.nav > li:hover, 
.nav > li:hover > a { 
  border-bottom-color: orange;
}
.nav li:hover > a { 
  color:orange; 
}
.nav > li:first-child { 
  border-radius: 4px 0 0 4px;
} 
.nav > li:first-child > a { 
  border-radius: 4px 0 0 0;
}
.nav > li:last-child { 
  border-radius: 0 0 4px 0; 
  margin-right: 0;
} 
.nav > li:last-child > a { 
  border-radius: 0 4px 0 0;
}
.nav li li a { 
  margin-top: 1px;
}

Then the magic happens:

.nav li a:first-child:nth-last-child(2):before { 
  content: ""; 
  position: absolute; 
  height: 0; 
  width: 0; 
  border: 5px solid transparent; 
  top: 50% ;
  right:5px;  
 }

That's essentially the active ingredient of this technique. For this example, I used the :before pseudo element off the anchor element to draw the arrows. The pseudo element is not necessary to the technique. I could have just as easily changed backgrounds on the anchor itself instead; you can make it do most anything you want once you have targeted the element.

Finally, just to polish it off, some positioning and arrow styling CSS code.

/* submenu positioning*/
.nav ul {
  position: absolute;
  white-space: nowrap;
  border-bottom: 5px solid  orange;
  z-index: 1;
  left: -99999em;
}
.nav > li:hover > ul {
  left: auto;
  margin-top: 5px;
  min-width: 100%;
}
.nav > li li:hover > ul { 
  left: 100%;
  margin-left: 1px;
  top: -1px;
}
/* arrow hover styling */
.nav > li > a:first-child:nth-last-child(2):before { 
  border-top-color: #aaa; 
}
.nav > li:hover > a:first-child:nth-last-child(2):before {
  border: 5px solid transparent; 
  border-bottom-color: orange; 
  margin-top:-5px
}
.nav li li > a:first-child:nth-last-child(2):before {  
  border-left-color: #aaa; 
  margin-top: -5px
}
.nav li li:hover > a:first-child:nth-last-child(2):before {
  border: 5px solid transparent; 
  border-right-color: orange;
  right: 10px; 
}

Here is the whole thing in action at CodePen:

See the Pen Marking sub-menued items (sexy version) by Ray Messina (@RayM) on CodePen

As you probably guessed, you could also use other selector/selector combinations such as :only-child, :first-child:last-child, :first-child:not(:last-child) and the like. But I have found that :nth-child(x):nth-last-child(x) gives the most flexibility and serves as a built-in fallback (as it allows us to target the element directly, rather than by exclusion), and there is little cross browser support advantage gained by using the other selector/selector combinations.

That's it. Simple, graceful and totally automatic; the way it ought to be. Support for this is almost universal, except for IE, which eats glue and only offers support in IE9+. At the time of writing, estimated global support for these selectors, as used, was roughly 87% which is not too bad.