Solved with CSS! Dropdown Menus

Avatar of Una Kravets
Una Kravets on (Updated on )

CSS is getting increasingly powerful, and with features like CSS grid and custom properties (also known as CSS variables), we’re seeing some really creative solutions emerging. Some of those solutions focus around not only making the web prettier, but making it more accessible, and making styling accessible experiences better. I’m definitely here for it!

Article Series:

  1. Colorizing SVG Backgrounds
  2. Dropdown Menus (this post)
  3. Logical Styling Based On the Number of Given Elements

A common UI pattern that we see on the web are dropdown menus. They’re used to display related information in pieces, without overwhelming the user with buttons, text, and options. Somewhere that we see these a lot is inside of headers or navigation areas on websites.

A collage of screenshots showing different dropdown menu examples.
A Google search for “dropdown menu” yields many examples

Let’s see if we can make one of these menus with CSS alone. We’ll create a list of links within a nav component like so:

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#">Two</a></li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

Now, say we want a sub-menu dropdown on the second navigation item. We can do the same thing there and include a list of links within that list item:

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#">Two</a>
      <ul class="dropdown">
        <li><a href="#">Sub-1</a></li>
        <li><a href="#">Sub-2</a></li>
        <li><a href="#">Sub-3</a></li>
      </ul>
    </li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

We now have our two-tiered navigation system. In order to have the content hidden and displayed when we want it to be visible, we’ll need to apply some CSS. All style properties have been removed from the following example for clarity on interaction:

li {
 display: block;
 transition-duration: 0.5s;
}

li:hover {
  cursor: pointer;
}

ul li ul {
  visibility: hidden;
  opacity: 0;
  position: absolute;
  transition: all 0.5s ease;
  margin-top: 1rem;
  left: 0;
  display: none;
}

ul li:hover > ul,
ul li ul:hover {
  visibility: visible;
  opacity: 1;
  display: block;
}

ul li ul li {
  clear: both;
  width: 100%;
}

Now, the submenu dropdown is hidden, but will be exposed and become visible when we hover over its correlating parent in the navigation bar. By styling ul li ul, we have access to that submenu, and by styling ul li ul li, we have access to the individual list items within it.

The Problem

This is starting to look like what we want, but we’re still far from finished at this point. Web accessibility is a core part of your product’s development, and right now would be the perfect opportunity to bring this up. Adding role="navigation" is a good start, but in order for a navigation bar to be accessible, one should be able to tab through it (and focus on the proper item in a sensible order), and also have a screen reader accurately read out loud what is being focused on.

You can hover over any of the list items and clearly see what is being hovered over, but this isn’t true for tab navigation. Go ahead and try to tab through the example above. You lose track of where the focus is visually As you tab to Two in the main menu, you’ll see a focus indicator ring, but when you tab to the next item (one of its submenu items), that focus disappears.

An animated screenshot showing focus rings on menu items as they are tabbed.

Now, it’s important to note that theoretically you are focused on this other item, and that a screen reader would be able to parse that, reading Sub-One, but keyboard users will not be able to see what’s going on and will lose track.

The reason this happens is because, while we’re styling the hover of the parent element, as soon as we transition focus from the parent to one of the list items within that parent, we lose that styling. This makes sense from a CSS standpoint, but it’s not what we want.

Luckily, there is a new CSS pseudo class that will give us exactly what we want in this case, and it’s called :focus-within.

The Solution: :focus-within

The :focus-within pseudo selector is a part of the CSS Selectors Level 4 Spec and tells the browser to apply a style to a parent when any of its children are in focus. So in our case, this means that we can tab to Sub-One and apply a :focus-within style along with the :hover style of the parent and see exactly where we are in the navigation dropdown. In our case it would be ul li:focus-within > ul:

ul li:hover > ul,
ul li:focus-within > ul,
ul li ul:hover {
  visibility: visible;
  opacity: 1;
  display: block;
}

Sweet! It works!

Quick detour! If you’re only supporting modern browsers, the CSS we’ve seen so far is fine. But you should know that when any browser doesn’t understand part of a selector, it throws the entire selector out. So if you want to support IE 11, you can’t mix in the :focus-within part.

/* This compound selector will still work in IE 11 because :focus-within isn't mixed in */
ul li:hover > ul,
ul li ul:hover,
ul li ul:focus {
  visibility: visible;
  opacity: 1;
  display: block;
}

/* IE 11 won't get this, but at least the top-level menus will work */
ul li:focus-within > ul {
  visibility: visible;
  opacity: 1;
  display: block;
}

Now, when we tab to the second item, our submenu pops up, and as we tab through the submenu, the visibility remains! Now, we can append our code to include :focus states alongside :hover to give keyboard users the same experience as our mouse users.

An animated screenshot of a menu showing the sybmenu being revealed when it is actively tabbed.

In most cases, such as on direct links, we usually can just write something like:

a:hover,
a:focus {
  ...
}

But in this case, since we’re applying hover styles based on the parent li, we can again utilize :focus-within to get the same look at feel when tabbing through. This is because we can’t actually focus on the li (unless we add a tabindex="0"). We’re actually focusing on the link (a) within it. :focus-within allows us to still apply styles to the parent li when focusing on the link (pretty darn cool!):

li:hover,
li:focus-within {
  ...
}
An animated screenshot showing a tabbed menu where the submenu is revealed when actively tabbed, the submenu items show the focus ring when active, and the hover styles are also applied when active.

At this point, since we are applying a focus style, we can do something that’s typically not recommended (remove the styling of that blue outline focus ring). We can do this by:

li:focus-within a {
  outline: none;
}

The above code specifies that when we focus within list items via the link (a), do not apply an outline to the link item (a). It’s pretty safe to write it this way, because we’re exclusively styling the hover state, and with browsers that do not support :focus-within, the link will still get a focus ring. Now our menu looks like this:

An animated screenshot showing the final result of the tabbed menu where the focus ring has been removed and replaced by the hover state when menu items are actively tabbed.
Final menu using :focus-within, :hover states, and customizing the focus ring to disappear

What About ARIA?

If you’re familiar with accessibility, you may have heard of ARIA labels and states. You can use these to your advantage to also create these types of dropdowns with built-in accessibility at the same time! You can find an excellent example here by Heydon Pickering. When including ARIA markup, your code would look a little more like this:

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#" aria-haspopup="true">Two</a>
      <ul class="dropdown" aria-label="submenu">
        <li><a href="#">Sub-1</a></li>
        <li><a href="#">Sub-2</a></li>
        <li><a href="#">Sub-3</a></li>
      </ul>
    </li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

You’re adding aria-haspopup="true" to the parent of the dropdown menu to indicate an alternative state, and including aria-label="submenu" on the actual dropdown menu itself (in this case our list with class="dropdown".

These properties themselves will give you the functionality you need to show the dropdown menu, but the downside is that they only work with JavaScript enabled.

Browser Support Caveat

Speaking of caveats, let’s talk about browser support. While :focus-within does have pretty good browser support, it’s important to note that Internet Explorer and Edge are not supported, so your users on those platforms will not be able to see the navigation.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
6052No7910.1

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12212312210.3

The ultimate solution here would be to use both ARIA markup and CSS :focus-within to ensure a solid dropdown experience for your users.

If you want to be able to use this feature in the future, please upvote it on Edge User Voice! And upvote :focus-ring while you’re at it, so that we’ll be able to style that focus ring and create a beautiful interactive web experience for all 😀

More on :focus-within and A11Y