teehan+lax logo svg

How to code a Teehan+Lax navigation

This type of navigation was first seen on the Teehan+Lax website, then it became quite popular and got adopted by several other websites (MailChimp, Wunderlist). The navigation bar works as follows: it’s being hidden once you start scrolling. As soon as you scroll up a few pixels it is shown. After that, when you scroll down again it gets hidden. When you have reached the bottom of the page the navigation bar reappears.

I saw it first, a couple of months ago, on Teehan+Lax. Since then, I have been waiting for a good out-of-the-box coded solution that would replicate this functionality to come out. But it didn’t, so I coded my own by reverse-engineering Teehan+Lax’s navigation. I’ll be using this solution for the redesign of this site (coming soon, hopefully).

Making it better

On top of the basic functionality that can be found on Teehan+Lax’s navigation, I coded a better version:

  • Enabled navigation only with a keyboard (Tab + Enter/Space).
  • Added keyboard shortcuts (Esc / M).
  • Fixed Firefox animation bugs.
  • Added functionality to close navigation if you click anywhere outside link area.

View the Demo →

Step 1 - HTML

We need some HTML code for the navigation. The navigation bar, .site-width (what is visible by default) is separated from the dropdown #navigation:

<nav>
  <div class="site-width">
    <a href="http://twitter.com/itsjaswinder" target="_blank">Jaswinder Singh</a>
    <span class="menu icon" title="Menu (Esc)" tabindex="0" data-icon="m"><span>Menu</span></span>
  <div>

  <div id="navigation">
    <ul>
      <li><span>The man behind this site</span><a href="#" class="about" title="About me">About me</a></li>
      <li><span>Pretty things I made</span><a href="#" class="portfolio" title="My Portfolio">My Portfolio</a></li>
      <li><span>Cool stories I wrote</span><a href="#" class="blog" title="My Blog">My Blog</a></li>
      <li><span>Experiments I conducted</span><a href="#" class="labs" title="My Lab">My Lab</a></li>
      <li><span>Where you can find me</span><a href="#" class="contact" title="Contact Me">Contact Me</a></li>
    </ul>
  </div>
</nav>

Step 2 - JavaScript

The logic behind the functionality is simple. Once the user has scrolled past the navigation, if they scroll back up, we show the navigation with a position:fixed so that it stays fixed on the screen.

We need three states for the navigation: ‘invisible’, ‘detached’, and ‘expanded’. The navigation becomes ‘invisible’ once you scroll past it. If you scroll back up we show a nice drop animation. If you scroll beyond a certain point, the navigation becomes ‘detached’, i.e. fixed on the screen. If you scroll back up the navigation will show the nice drop animation but because of the .detached class, only a few pixels above instead of at the beginning of the site. Lastly, if the dropdown is shown, the navigation has an .expanded class.

To achieve that we need four variables:

var previousScroll = 0, // previous scroll position
    menuOffset = 54, // height of menu (once scroll passed it, menu is hidden)
    detachPoint = 650, // point of detach (after scroll passed it, menu is fixed)
    hideShowOffset = 6; // scrolling value after which triggers hide/show menu

Don’t make the detachPoint value too small, because if the user scrolls fast, the class .detached will be added before having the chance to hide the navigation, resulting in a show/hide flicker of the navigation.

We’ll write efficient code. On scroll, only if the navigation is not shown we’ll compute its position and whether it will have position:fixed or not.

// on scroll hide/show menu
$(window).scroll(function() {
  if (!$('nav').hasClass('expanded')) {
})

Inside the else {} we need to calculate if the user scrolled past the navigation’s height, if they scrolled past the detachPoint, if they’re scrolling up or down, how fast they’re scrolling, and if they are at the bottom of the site (we’ll show navigation again):

var currentScroll = $(this).scrollTop(), // gets current scroll position
    scrollDifference = Math.abs(currentScroll - previousScroll); // calculates how fast user is scrolling

// if scrolled past menu
if (currentScroll > menuOffset) {
  // if scrolled past detach point add class to fix menu
  if (currentScroll > detachPoint) {
    if (!$('nav').hasClass('detached'))
      $('nav').addClass('detached');
  }

  // if scrolling faster than hideShowOffset hide/show menu
  if (scrollDifference >= hideShowOffset) {
    if (currentScroll > previousScroll) {
      // scrolling down; hide menu
      if (!$('nav').hasClass('invisible'))
        $('nav').addClass('invisible');
    } else {
      // scrolling up; show menu
      if ($('nav').hasClass('invisible'))
        $('nav').removeClass('invisible');
    }
  }
} else {
  // only remove “detached” class if user is at the top of document (menu jump fix)
  if (currentScroll <= 0){
    $('nav').removeClass();
  }
}

// if user is at the bottom of document show menu
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
  $('nav').removeClass('invisible');
}

// replace previous scroll position with new one
previousScroll = currentScroll;

Next we need a function to check if we’re showing or hiding the dropdown of the navigation:

// checks if navigation’s popover is shown
function showHideNav() {
  if ($('nav').hasClass('expanded')) {
    hideNav();
  } else {
    showNav();
  }
}

Next we create functions to show or hide the navigation dropdown. We a add class to the navigation, another class to the content (to blur it), and a class to the body in order to disable scrolling of the content whilst the navigation’s dropdown is shown. We add a delay to this because of Firefox, when the class is added, the dropdown animation is not shown.

Lastly, you should be able to navigate with the TAB key on your keyboard. We don’t want the user to select the links with the TAB key when the navigation is hidden:

// shows the navigation’s popover
function showNav() {
  $('nav').removeClass('invisible').addClass('expanded');
  $('#container').addClass('blurred');
  window.setTimeout(function(){$('body').addClass('no_scroll');}, 200); // Firefox hack. Hides scrollbar as soon as menu animation is done
  $('#navigation a').attr('tabindex', ''); // links inside navigation should be TAB selectable
}

// hides the navigation’s popover
function hideNav() {
  $('#container').removeClass('blurred');
  window.setTimeout(function(){$('body').removeClass();}, 10); // allow animations to start before removing class (Firefox)
  $('nav').removeClass('expanded');
  $('#navigation a').attr('tabindex', '-1'); // links inside hidden navigation should not be TAB selectable
  $('.icon').blur(); // deselect icon when navigation is hidden
}

Now we just need to define when to show the dropdown and when to hide it (clicking/tapping anywhere outside link area). To disable hiding the navigation when you click/tap inside the link area (say you select some text), we need to stopPropagation():

// shows/hides navigation’s popover if class "expanded"
$('nav').on('click touchstart', function(event) {
  showHideNav();
  event.preventDefault();
})

// clicking anywhere inside navigation or heading won’t close navigation’s popover
$('#navigation').on('click touchstart', function(event){
    event.stopPropagation();
})

Lastly, we add the possibility to navigate with the keyboard. If the menu icon is focused and you press Enter/Space then we show the navigation’s dropdown. If you press Esc or M we do the same thing.

// keyboard shortcuts
$('body').keydown(function(e) {
  // menu accessible via TAB as well
  if ($("nav .icon").is(":focus")) {
    // if ENTER/SPACE show/hide menu
    if (e.keyCode === 13 || e.keyCode === 32) {
      showHideNav();
      e.preventDefault();
    }
  }

  // if ESC show/hide menu
  if (e.keyCode === 27 || e.keyCode === 77) {
    showHideNav();
    e.preventDefault();
  }
})

Step 3 CSS

I won’t go too deep into styling the navigation. You can browse through the CSS code on GitHub. The most important styles you need are for the three states of the navigation: invisible, detached, and expanded.

nav {
  color: #333;
  position: absolute;
  top: 0;
  width: 100%;
  height: 46px;
  padding-top: 8px;
  right: 0;
  z-index: 1000;
  cursor: pointer;
  overflow: hidden;
  -webkit-transform: translate(0,0);
  -moz-transform: translate(0,0);
  -o-transform: translate(0,0);
  transform: translate(0,0);
  -webkit-transition: -webkit-transform .4s, height .3s, background .4s;
  -moz-transition: -moz-transform .4s, height .3s, background .4s;
  transition: transform .4s, height .3s, background .4s;
}

/* when hidden it goes up */
nav.invisible {
  -webkit-transform: translate(0,-64px);
  -moz-transform: translate(0,-64px);
  -o-transform: translate(0,-64px);
  transform: translate(0,-64px);
  -webkit-transition: -webkit-transform .2s;
  -moz-transition: -moz-transform .2s;
  -o-transition: -o-transform .2s;
  transition: transform .2s;
  opacity: 0;
}

/* when shown & detached position is fixed */
nav.detached {
  position: fixed;
  background: rgba(255,255,255,.9);
  -webkit-transition: -webkit-transform .3s, height .3s, background .4s, opacity .3s;
  -moz-transition: -moz-transform .3s, height .3s, background .4s, opacity .3s;
  -o-transition: -o-transform .3s, height .3s, background .4s, opacity .3s;
  transition: transform .3s, height .3s, background .4s, opacity .3s;
}

/* increases menu width & height */
nav.expanded {
  width: 100%;
  height: 100%;
  position: fixed;
  cursor: default;
  background: rgba(255,255,255,.85);
}

/* positions navigation content */
#navigation {
  -webkit-transform: translate(0,-700px);
  -moz-transform: translate(0,-700px);
  -o-transform: translate(0,-700px);
  transform: translate(0,-700px);
  opacity: 0;
  padding-top: 2em;
  text-align: center;
  -webkit-transition: -webkit-transform .15s, opacity .7s;
  -moz-transition: -moz-transform .15s, opacity .7s;
  -o-transition: -o-transform .15s, opacity .7s;
  transition: transform .15s, opacity .7s;
}

/* shows navigation */
nav.expanded #navigation {
  -webkit-transform: translate(0,0);
  -moz-transform: translate(0,0);
  -o-transform: translate(0,0);
  transform: translate(0,0);
  opacity: 1;
}

Find the rest of the CSS code on GitHub.

Conclusion

It’s fairly straightforward to create a navigation like Teehan+Lax have. Where you need to pay attention is on the timing of the animations. We don’t want to annoy the user, rather delight them and improve their experience.

Read next

Design for the future, not the past