chrisronline.com

Me

Hi. I'm Chris. I write code. I love open source and am actively working on contributing more.

I am currently helping Hudl dominate.

Thoughts

  • 07/04/2013

    Fluid CSS Tertiary Navigation

    The Problem

    I recently fixed a navigation issue at work that involved a tiered, CSS-driven dropdown/flyout menu. The menu worked properly using the CSS :hover selector but it did not support localization well and because of that, the CSS used "worse-case" hard-coded widths to ensure all locales were supported. In languages where the navigation text was much shorter than the "worse-case", the second tier of the menu used a width much too large for the text.

    The Solution

    See the Pen AkJze by Chris Roberson (@chrisronline) on CodePen.

    I designed a fluid-width, CSS-based tertiary menu system that contains no hard-coded widths or assumptions about sizing.

    How the worse case scenario looked in the new CSS
    Another part of the menu system in the new CSS, episode guide, season 2

    Build

    Let's go through the process of building this menu from scratch but first, let's define some goals

    Goals

    1. No explicit widths
    2. Support in IE8, Chrome, Safari and Firefox
    3. No javascript

    Basic

    We will start by providing some barebones HTML for the menu

    <ul class="primary">
      <li><a href="#">home</a></li>
      <li><a href="#">characters</a></li>
      <li><a href="#">media</a></li>
      <li><a href="#">episode guide</a></li>
    </ul>

    Now let's apply some basic CSS to create a horizontal menu

    ul {
      list-style: none;
      padding: 0;
      margin: 0;
      font: normal 16px Tahoma;
    }
    li {
      padding: 0.4em;
      margin: 0.4em 0;
      float: left;
    }

    Secondary Navigation

    Now, let's add a secondary navigation menu for 'characters'. Considering accessibility concerns, a solid approach is to nest another list within the list item tag so a device that renders this without any styles will show a nice, tiered list.

    <li>
      <a href="#">characters</a>
      <ul class="secondary">
        <li><a href="#">michael scott</a></li>
        <li><a href="#">jim halpert</a></li>
        <li><a href="#">dwight schrute</a></li>
        <li><a href="#">pam beasley</a></li>
      </ul>
    </li>

    But wait! Nesting a block-level element in a fluid container will cause the container to expand its width to contain the new element which in turns increases the width of the primary navigation element. There is no fix for that, sorry...

    ...

    ...

    Okay obviously there is a fix and that fix is

    ul.secondary {
      position: absolute;
    }

    This accomplishes two things:

    1. The HTML layout ignores the element when positioning everything
    2. The original positioning of the ul.secondary element is not changed

    The secondary navigation list is supposed to be vertical and since our tertiary navigation list will be the same, change the styling for all lis and then override the one for the primary navigation bar.

    li {
      padding: 0.4em;
      margin: 0.4em 0;
    }
    .primary > li {
      float: left;
    }

    Note that the selector for the primary navigation bar only targets children li elements and does not consider further descendants which is important because secondary and tertiary menus are all children of .primary

    Tertiary Navigation

    Now that we have a working secondary navigation level, let's investigate the tertiary level. Using the exact same approach as the secondary menu, we will nest another list within a secondary list item.

    <ul class="secondary">
    <li>
      <a href="#">michael scott</a>
      <ul class="tertiary">
        <li><a href="#">bio</a></li>
        <li><a href="#">pictures</a></li>
        <li><a href="#">video</a></li>
        <li><a href="#">contact</a></li>
      </ul>
      <div class="clear"></div>
    </li>

    Add the same positioning to the tertiary list as the secondary list

    ul.tertiary {
      position: absolute;
    }

    Uh...that did not work as well as last time.

    The output of setting position:absolute for the tertiary list

    The problem is that .tertiary is positioned in relation to the same container as .secondary. It does not appear in the exact same position because if you simply assign an element position:absolute, it appears in the normal flow of the page.

    The solution is to set the left position of the tertiary element to 100% which will push it back to the original horizontal position before applying the position:absolute

    ul.tertiary {
      position: absolute;
      left: 100%;
    }
    The output of fixing the absolute issue with left:100%

    Final

    We're nearly there, but the tertiary navigation container is not correctly positioned. We can see this clearly if add some color into our navigation bar.

    li {background-color: yellow; }
    a { background-color: pink; }
    .primary { background-color: green; }
    .secondary { background-color: orange; }
    .tertiary { background-color: blue; }
    The teritary container is mispositioned

    The top section of the tertiary container still appears relative to the original placement in the page (under the a tag) so simply give it a top:0 and it will position at the top.

    ul.tertiary {
      position: absolute;
      left: 100%;
      top: 0;
    }

    which then results in:

    The teritary container is now properly aligned at the top

    The tertiary container is now correctly placed! Let's hook up the CSS hover functionality and add some more content

    ul {
      display: none;
    }
    
    ul.primary,
    ul.primary li:hover ul.secondary,
    ul.secondary li:hover ul.tertiary {
      display: block;
    }

    Everything looks great at first glance, but there are two issues with the tertiary navigation containers...

    Whitespace issue with a tertiary link
    Second tertiary navigation container appears in the same location as the first one

    The first fix involves the white-space CSS property and setting this property to 'nowrap' causes the browser to ignore any space limitations on the parent container and allows its content to flow on a single line. This will fix our issue in modern browsers (IE8+)

    The second fix is a positioning issue because the nearest relative positioned element for the second tertiary container to position itself to is the same as the first tertiary container - we just need to make the lis position:relative. However, this reintroduces the top positioning issue but we can easily fix that by setting the top property equal to the padding of the lis

    Whitespace issue with a tertiary link is now fixed
    Second tertiary navigation container appears in the correct location

    The last remaining issue is a small offset for the top of the secondary navigation containers in relation to the parent lis in the primary navigation. The fix is super simple:

    ul.secondary {
      position: absolute;
      top: 100%;
    }

    All Done

    Whew...finally done! We now have a fluid, CSS-only navigation bar that supports secondary and tertiary levels of navigation!

    Credits

    I'd like to thanks Alexandra Atzl for pointing out a few issues with my original solution

Work

Public repositories listed on GitHub