A Responsive Design Approach for Navigation, Part 1

April 2020 note: Hi! Just a quick note to say that this post is pretty old, and might contain outdated advice or links. We're keeping it online, but recommend that you check newer posts to see if there's a better approach.

As we create responsive websites, we must consider a number of factors to make sure both the design and code are as bullet-proof as possible: the design must scale across a wide range of screen sizes from mobile to tablet to desktop; and the code must start with a mobile-first approach, work well for screen readers or with JavaScript disabled, and be robust enough to adapt to differences in text size and rendering across devices or user settings.

Adapting site navigation to be understandable, usable and attractive across a wide range of devices is particularly challenging. On the Boston Globe site, we gained some key insights about how to build robust mobile-first responsive navigation systems. This is the first in a series of articles in which we’ll explore navigation design techniques from very simple nav bar design, to complex multi-level hierarchies, search, and fuller featured navigation systems.

Truly responsive navigation

Permalink to 'Truly responsive navigation'

Using progressive enhancement, CSS media queries, and a little JavaScript, we built a navigation list that adjusts to fit the size of the screen and adapts to differences in text sizing. View the demo

We’re in the process of rethinking our site design, so we used our own navigation list as a test case. It consists of 5 options — What We Do, What We’ve Done, What We’re Thinking, Who We Are, and Contact Us. These options can take up a lot of space on a tiny mobile screen, so our goal was simple: for smaller screens we’ll make the navigation as compact as possible to save room for the main content, and on larger screens with ample space we’ll display all options in a horizontal bar for quicker access.

We marked up a standard unordered list of links, usable on any device that renders HTML, layered on a few mobile-friendly styles to make the hit area bigger for finger taps, and then added a little JavaScript to transform the list into a custom dropdown menu. Users that don’t have JavaScript enabled will just see the full list — no harm, no foul.

For larger screens, we used CSS3 media queries to display the options in a horizontal bar. We wanted to ensure that the screen has enough room to display the entire bar in a single line (we don’t want the nav or its options to wrap because, in this case, wrapping would make the page look broken or mess with the navigation hierarchy), so we estimated the minimum width value: width of the horizontal navigation bar + the width of any neighboring elements + a little wiggle room. We tested this breakpoint across a wide range of browsers, and increased the value when we saw options wrap.

We could’ve stopped there, but we can’t forget that screen size is just one of many factors that effect how a layout is displayed. Take text rendering, for example: the type, size, and shape of fonts can vary among operating systems/browsers, and users have the option to adjust sizing with browser controls. (And, as Stephanie Rieger notes, the list of user-controlled variables is growing.) So, while it makes sense to use media queries as a baseline for altering the layout, we made our navigation bar feel smarter and more responsive with a little JavaScript to detect whether the horizontal nav bar fits in the available space, regardless of screen size; when the fit test fails, the navigation remains a custom dropdown menu.

The final result is a navigation list that’s optimized for use on a wide range of screen widths and with variable text sizes. We’ll walk through this example in detail below.

Markup

Permalink to 'Markup'

The menu is a list of links, so that’s what we’ll use: a standard unordered list with anchor tags. We’ll group the list with a heading to identify the navigation element, “Sections”, and give the outer container a descriptive class for scoping styles, nav-primary.

<div class="nav-primary">
   <h3>Sections:</h3>
    <ul>
        <li><a href="http://www.filamentgroup.com/services">What We Do</a></li>
        <li><a href="http://www.filamentgroup.com/portfolio">What We've Done</a></li>
        <li><a href="http://www.filamentgroup.com/lab">What We're Thinking</a></li>
        <li><a href="http://www.filamentgroup.com/about">Who We Are</a></li>
        <li><a href="http://www.filamentgroup.com/contact">Contact Us</a></li>
    </ul>
</div>

We’ll assign the class nav-current to the active navigation option; this will come in handy later when we create the custom dropdown menu.

<li class="nav-current"><a href="http://www.filamentgroup.com/services">What We Do</a></li>

To round out our example, we’ll add the Filament Group logo and a small block of content. For screen readers we’ll provide a “skip navigation” link so that they don’t have to navigate through the list on every page.

<a href="http://www.filamentgroup.com/" id="logo"><img src="fg-logo.gif" alt="Filament Group, Inc." /></a>

<a href="#main" class="skip">Skip navigation</a>

<div class="nav-primary">
   <h3>Sections:</h3>
    <ul>
        <li class="nav-current"><a href="http://www.filamentgroup.com/services">What We Do</a></li>
        <li><a href="http://www.filamentgroup.com/portfolio">What We've Done</a></li>
        <li><a href="http://www.filamentgroup.com/lab">What We're Thinking</a></li>
        <li><a href="http://www.filamentgroup.com/about">Who We Are</a></li>
        <li><a href="http://www.filamentgroup.com/contact">Contact Us</a></li>
    </ul>
</div>

<div class="content">
   <p>We offer services from strategy to UI and application design, to accessible front-end code development, and happily work with clients to find the right mix that supports their internal capabilities.</p>
</div>

Before we wrap up the markup, we want to make sure that our page content is sized to fit the screen, so we’ll add a viewport meta tag (how the viewport meta tag works):

<meta name="viewport" content="width=device-width, initial-scale=1">

We now have a legible, usable page to which we can add style and behavior enhancements.

Basic markup rendered on a mid-sized
screen

Styles for the small screen

Permalink to 'Styles for the small screen'

If you’re not familiar with using CSS3 media queries to render pages responsively, we highly recommend Ethan Marcotte’s definitive article, Responsive Web Design, and book by the same title.

We’ll start with a few basic styles that make the links easier to read and tap on small screens (a block display property and padding, and a font-size that’s large enough for finger-based gestures), and a few additional styles to make the list easier to scan (borders between options, and bold text for the active option). We’ll also hide the “Sections” heading (h3), but in a way that keeps it accessible to screen readers.

.nav-primary h3 {
   position: absolute;
   left: -999em;
}
.nav-primary ul {
   border: 1px solid #e6e6e6;
}
.nav-primary li {
   font-size: 1.8em;
   border-bottom: 1px solid #eee;
}
.nav-primary li:last-child {
   border-bottom: 0;
}
.nav-primary a {
   display: block;
   padding: .5em .8em;
   text-decoration: none;
   color: #333;
}
.nav-primary a:hover {
   background-color: #f8f8f8;
}
.nav-primary .nav-current {
   font-weight: bold;
}

We’ll add a few more rules to style the overall page, logo and content blocks, and another rule to make the navigation play nicely with them (clear the logo, and add space before the content). We want the “skip” link to be available to screen readers but hidden from everyone else, so we’ll position it off the page.

body {
    font: 62.5%/1.4 helvetica, arial, sans-serif;
    padding: 2em 1em;
}
#logo {
   float: left;
   margin: 0 0 1em;
}
.content {
   clear: both;
}
p {
   font-size: 1.8em;
   line-height: 1.4;
   margin: 0 .3em 1em;
}
a.skip {
   position: absolute;
   left: -999em;
}

...

.nav-primary {
   clear: left;
   margin: 0 0 2em;
}

Basic styles rendered on a small
screen

…and for larger screens, like tablets and desktops

Permalink to '…and for larger screens, like tablets and desktops'

Our page looks nice at smaller screen widths, but on a larger screen, each option stretches to fill the space and the navigation dominates the page.

Basic styles rendered on a larger screen; the navigation takes up too
much space

To fix this, we’ll add a couple of rules, scoped within a media query, to display the navigation as a horizontal bar in screens that are 640px wide and larger (640 = full width of the navigation + body padding).

We’ll float each navigation option to the left, remove the border between options, and assign a slightly smaller font size. We’ll also float the navigation block itself and the list container so that they fully contain their floated child elements.

@media screen and (min-width: 640px) {
   .nav-primary,
   .nav-primary ul {
      float: left;
   }
   .nav-primary ul {
      float: left;
   }
   .nav-primary li {
      float: left;
      font-size: 1.5em;
      border-bottom: 0;
   }
}

Much better.

Navigation rendered as a horizontal bar on screens 640px or
wider

When the screen is wide enough to fit the logo and nav side-by-side — 910px, specifically — we’ll float the navigation block to the right. We arrived at that number by adding the width of the logo (250px) to the width of the navigation list displayed horizontally (640px), plus a little extra margin to account for slight browser rendering differences (20px).

@media screen and (min-width: 910px) {
   .nav-primary {
      float: right;
      clear: none;
   }
}

Our navigation now assumes one of two forms in response to the screen’s size: a list on smaller screens, or a horizontal bar on larger screens.

Examples showing a list on smaller screens, or a horizontal bar on
larger screens

As it stands, our navigation looks pretty good on a range of screen sizes — but there is definitely room for improvement. On small screens, our navigation list still takes up a good chunk of vertical space, and on larger screens, our horizontal navigation bar wraps when we increase our browser’s text size. Bump up the text a couple of times on a tablet-sized screen and the options wrap to a second line:

Navigation options wrap to a second line when text size is
increased

And at larger sizes, the entire navigation bar wraps under the logo, creating odd gaps of white space:

Navigation bar wraps under the logo when text size is
increased

So we have two issues left to address:

  • transform the navigation into a compact, custom dropdown menu when screen space is limited, and
  • create a test to determine if navigation options will fit on a single line, or if the entire bar will fit next to the logo on larger screens; if not, show the compact menu.

We’ll start with menu styles, and then fill in the test logic.

Styles for a compact menu

Permalink to 'Styles for a compact menu'

We’ll write styles to hide all options by default except for the active option, nav-current, and display the “Sections” heading as a clickable dropdown button positioned to the right. We’ll add another class, expanded, that we’ll toggle with JavaScript to show/hide the full list of options.

Note that these styles are all scoped to the class, nav-menu. Later, our test logic will toggle this class on the body tag.

.nav-menu .nav-primary {
   margin: 0 0 1.5em;
   position: relative;
   overflow: hidden;
}
.nav-menu .nav-primary a {
   padding-right: 3em;
}
.nav-menu .nav-primary h3 {
   position: absolute;
   top: 0;
   left: auto;
   right: 0;
   display: block;
   width: 4em;
   height: 4.5em;
   background: #ccc url(img/icons.png) no-repeat -205px 45%;
   text-indent: -999em;
   cursor: pointer;
}
.nav-menu .nav-primary.expanded h3 {
   background-position: -169px 45%;
}
.nav-menu .nav-primary li {
   clear: left;
   display: none;
}
.nav-menu .nav-primary.expanded li,
.nav-menu .nav-primary li.nav-current {
   display: list-item;
}
.nav-menu .nav-primary li.nav-current {
   border-bottom-width: 0;
}
.nav-menu .nav-primary.expanded li.nav-current {
   border-bottom-width: 1px;
}

Now, when JavaScript is enabled, the menu will take a more compact form:

Navigation rendered as a compact
menu

Next, we’ll write a bit of JavaScript to ensure that our navigation bar fits the available space.

Does the navigation fit?

Permalink to 'Does the navigation fit?'

We’ll bind a custom event, “testfit”, to the navigation container. We’ll trigger this event when the page loads, and when the screen changes size or orientation. (A nice side effect: desktop browsers that support text zooming, like the latest versions of Chrome, Firefox, and Opera, trigger the resize event — and this test — when the user increases or decreases text sizing with browser controls or key commands.)

$('.nav-primary')
   // test the menu to see if all items fit horizontally
   .bind('testfit', function(){
      // ...logic goes here...
   })

   // ...and update the nav on window events
   $(window).bind('load resize orientationchange', function(){
      $('.nav-primary').trigger('testfit');
   });

When the test passes, we’ll add the nav-menu class to the body. One of the following conditions must be met for “testfit” to pass: when the entire nav wraps to a second line under the logo, or when any of the navigation options wrap to a second line. In both cases, we test for wrapping by comparing top offset values, which can be effected by changes in screen or text size. When the navigation’s top value is greater than the logo’s, the navigation has wrapped to the next line; when the last navigation option’s top value is greater than the first, it’s wrapped.

We’ll also remove the nav-menu class from the body to reset the styles before the test is run.

$('.nav-primary')
   // test the menu to see if all items fit horizontally
   .bind('testfit', function(){
      var nav = $(this),
            items = nav.find('a');

      $('body').removeClass('nav-menu');

      // when the nav wraps under the logo, or when options are stacked, display the nav as a menu
      if ( (nav.offset().top > nav.prev().offset().top) || ($(items[items.length-1]).offset().top > $(items[0]).offset().top) ) {

         // show the menu -- add a class for scoping menu styles
         $('body').addClass('nav-menu');

      };
   })

   ...

Finally, we’ll assign a click event to the heading element to show/hide the menu options.

$('.nav-primary').find('h3')
   .bind('click focus', function(){
      $(this).parent().toggleClass('expanded')
   });

We now have a menu that is responsive to differences in screen size (view the demo):

Examples of the closed menu and with all options visible, sized for
the tablet screen, and displayed as a horizontal bar next to the
logo

…and text size:

On larger screens, the navigation becomes a compact menu when text
size is increased and the nav no longer fits on one
line

Keep the conversation going

Permalink to 'Keep the conversation going'

The pattern discussed here is one possibility for coding a responsive navigation list. We hope to discover more, and will update our RWD-Nav-Patterns git repository as we build additional examples.

This demo code is open source and available for download. Feel free to put it through its paces. If you use it and see room for improvement, please submit a pull request and we’ll review it as soon as possible.

All blog posts