Build a Responsive Nav BBC & Smashing Magazine Style!!
Tags: responsive, nav, intersectionobserver, bbc
Published: 2020-09-15 19:10:10

---
title: Build a Responsive Nav BBC & Smashing Magazine Style!!
published: true
description: A tutorial & reference implementation for building responsive navs with IntersectionObserver or pure CSS.
tags: responsive, nav, intersectionobserver, bbc
//cover_image: https://direct_url_to_image.jpg
---

### Context

While re-designing a friend's site from scratch recently I decided I wanted to do something different than the ol' hamburger for a responsive nav. I really like how [Smashing Magazine](https://smashingmagazine.com) has designed theirs - with nav items gradually *disappearing* from the main nav and *appearing* within the secondary nav as the viewport width decreases.

For a while I just assumed they were using JavaScript; however, after turning JS off while browsing their site I was surprised to see it still working! It turns out they use `grid` and a series of `media` queries to get their desired effect. I've recreated the basic functionality in [this codepen](https://codepen.io/tmns/pen/QWNmJar) - feel free to use it as a reference for building your own!

[BBC](https://www.bbc.com/) goes the JS route but with progressive enhancement so things don't break completely under JS-less conditions. Thankfully, some wonderful folks there have [published guidelines and mini-tutorials](https://bbc.github.io/gel/) for many of their components (with accessibility in mind!!). If I'm not mistaken, the supernaturally awesome [Heydon Pickering](https://twitter.com/heydonworks) participated in this, so you know it's quality!

The BBC folks call this component the [Masthead](https://bbc.github.io/gel/components/masthead/) and it is a requirement that it still serves its purpose (ie, to provide users with a set of navigation links) under the following circumstances:
> 1. No JavaScript: The functionality must be available to users whose browsers are not running JavaScript
2. JavaScript but no IntersectionObserver: The IntersectionObserver API is the most efficient way to allot a suitable number of promoted navigation links within the available space. These links must still be available where the browser does not support IntersectionObserver.

With those rules in mind, let's get started building our own version! At any time throughout the process you can refer to [this codepen](https://codepen.io/tmns/pen/QWNxjPm) to ensure you're on the right track!

### Adding some HTML & CSS

Since we want to progressively enhance our nav, we will start out by building the most simple version that will always display all nav items, in order to ensure that all users will be able to access all of our information!

Let's begin with the HTML:

```html
<header>
  <nav aria-label="Our Site">
    <a href="">
      <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <path d="M12 14l9-5-9-5-9 5 9 5z"></path>
        <path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"></path>
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"></path>
      </svg>
    </a>
    <div class="main-nav">
      <ul>
        <li data-index="0">
          <a href="">Link 1</a>
        </li>
        <li data-index="1">
          <a href="">Link 2</a>
        </li>
        <li data-index="2">
          <a href="">Link 3</a>
        </li>
        <li data-index="3">
          <a href="">Link 4</a>
        </li>
        <li data-index="4">
          <a href="">Link 5</a>
        </li>
        <li data-index="5">
          <a href="">Link 6</a>
        </li>
        <li data-index="6">
          <a href="">Link 7</a>
        </li>
        <li data-index="7">
          <a href="">Link 8</a>
        </li>
        <li data-index="8">
          <a href="">Link 9</a>
        </li>
        <li data-index="9">
          <a href="">Link 10</a>
        </li>
        <li data-index="10">
          <a href="">Link 11</a>
        </li>
      </ul>
    </div>
  </nav>
</header>
```

That's it, a `header` where I'll apply some wrapper-type styles, the first svg I saw from [Heroicons](https://heroicons.dev/), and the `nav` itself consisting of a `ul` with eleven items. Note we're also adding a custom `data-index` attribute to each list item, which we will use later when toggling styles with `IntersectionObserver`.

Now, you will probably notice that it's not the prettiest nav in the world; in fact, it looks just like a measly ol' list (because that's all a nav typically is!). Let's improve that a bit by adding some CSS (keep in mind, the styles here are written with Codepen's "Reset" base styles also applied):

```css
header {
  position: relative;
  margin-right: auto;
  margin-left: auto;
  background-color: burlywood;
  padding: 1rem 4rem 2rem;
  max-width: 64rem;
  color: darkslategray;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: 900;
}

nav {
  display: flex;
  position: relative;
  align-items: center;
  justify-content: center;
}

svg {
  width: 5rem;
  height: 5rem;
}

nav div {
  margin-left: 3rem;
}

ul {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  padding: 1rem;
}

li {
  padding-top: 1rem;
  padding-left: 2rem;
}

a {
  display: block;
  position: relative;
  text-decoration: none;
  white-space: nowrap;
  color: inherit;
}

a:focus {
  outline: 2px dashed white;
  outline-offset: 0.25rem;
}

ul a:hover {
  transform: rotate(-3deg);
}
```

Okay! That's a little nicer now! It should be noted here that all of the styles (eg spacing, color, hover effects) are completely arbitrary. The only real important bit is the setting of `flex` and `flex-wrap: wrap` on our `ul`. This enables the nav items to wrap as the viewport width decreases. And with that, our most basic responsive nav is complete!

Alternatively, if you want to avoid `flex` you can accomplish the same wrapping behavior by going old school and setting `float: left` on all our elements. We'll stick with `flex` here though.

Now let's add some extra functionality in the case that the user has JS enabled. We can add another item to the main nav that will act as a type of secondary nav, where we will "hide" items from the main nav as the viewport width decreases.

Let's place the markup for this extra item before the closing tag for our `nav`:

```html
[...]
    </div>
    <div class="nav__more-item" style="display: none;">
      <a href="#secondary-nav" role="button" aria-haspopup="true" aria-expanded="false">
        <span class="visually-hidden">More</span>
        <span aria-hidden="true">More</span>
      </a>
    </div>
  </nav>
</header>
```

Note that the entire `div` has a style attribute with `display: none;`. Remember, if the user does not have JS, we don't want to show this piece of functionality; so, we set its default `display` value to `none` and we will change it later via JS.

The CSS for this piece will consist of a little hover effect and an accessible way of visually hiding an element, which I learned from [Andy Bell](https://twitter.com/hankchizljaw):

```css
.nav__more-item {
  padding-top: 1rem;
}

.nav__more-item a:hover {
  background: darkslategray;
  color: burlywood;
}

.visually-hidden {
  position: absolute;
  margin: 0;
  border: 0;
  padding: 0;
  width: 1px;
  height: auto;
  overflow: hidden;
  white-space: nowrap;
  clip: rect(0 0 0 0);
}
```

Now let's add the HTML for the actual menu itself, which we will place right outside our `nav`, before the closing `header` tag:

```html
[...]
  </nav>
  <div id="secondary-nav" class="secondary-nav" style="display: none;">
    <ul class="secondary-nav__list">
      <li data-index="0">
        <a href="">Link 1</a>
      </li>
      <li data-index="1">
        <a href="">Link 2</a>
      </li>
      <li data-index="2">
        <a href="">Link 3</a>
      </li>
      <li data-index="3">
        <a href="">Link 4</a>
      </li>
      <li data-index="4">
        <a href="">Link 5</a>
      </li>
      <li data-index="5">
        <a href="">Link 6</a>
      </li>
      <li data-index="6">
        <a href="">Link 7</a>
      </li>
      <li data-index="7">
        <a href="">Link 8</a>
      </li>
      <li data-index="8">
        <a href="">Link 9</a>
      </li>
      <li data-index="9">
        <a href="">Link 10</a>
      </li>
      <li data-index="10">
        <a href="">Link 11</a>
      </li>
    </ul>
  </div>
</header>
```

Again, note we are setting its default `display` to `none`.

Now let's add some CSS just to make it a little nicer on the eyes:

```css
.secondary-nav__list {
  position: absolute;
  top: 75%;
  right: 0;
  flex-direction: column;
  align-items: center;
  transform: translateX(-7%);
  border-radius: 0.77rem;
  box-shadow: 0 13px 27px -5px rgba(50, 50, 93, 0.25),
    0 8px 16px -8px rgba(0, 0, 0, 0.3), 0 -6px 16px -6px rgba(0, 0, 0, 0.025);
  background-color: darkslategray;
  padding: 1rem 2rem 2rem;
  min-width: 5rem;
  height: auto;
  color: burlywood;
}

.secondary-nav__list::before {
  content: '';
  display: block;
  position: absolute;
  top: 1px;
  left: 50%;
  transform: translate(-50%, -100%);
  border-right: 15px solid transparent;
  border-bottom: 15px solid darkslategray;
  border-left: 15px solid transparent;
  width: 0;
  height: 0;
}

.secondary-nav__list li {
  padding-left: 0;
}
```

And with that we've actually written all the HTML and CSS necessary for our nav. From here on out we will just be applying bits of CSS via JS. Let's get to it!

### Adding some JS

Nearly all of the JS here is directly based on the JS written for the demo masthead linked above. So all credit goes to the folks who had a helping hand in creating it. It's also included unminified and with helpful comments, so [check it out](https://bbc.github.io/gel/static/js/gel.js) if you need more clarification on something.

Let's kick our version off by setting some essential styles. We will: 
* Set `overflow-x` of our nav to `auto` so the user will be able to still scroll and see all the menu items if their browser does not support `IntersectionObserver` . This style will be set to `hidden` later if `IntersectionObserver` is supported.
* Set `flex-wrap` of the nav list to `nowrap` so the items don't just simply wrap when the viewport width decreases.
* Set `display` block on our "More" nav item.

```javascript
var mainNav = document.querySelector('.main-nav');
mainNav.style.overflowX = 'auto';
mainNav.firstElementChild.style.flexWrap = 'nowrap';

var moreMenuItem = document.querySelector('.nav__more-item');
moreMenuItem.style.display = 'block';
```

Great! You should now be seeing the More nav item we added in the previous section. Now let's add an event listener to it that shows / hides the secondary nav menu on `click`:

```javascript
var moreMenuLink = moreMenuItem.querySelector('a');
var secondaryNav = document.querySelector('.secondary-nav');

moreMenuLink.addEventListener('click', function (e) {
  e.preventDefault();
  
  var expanded = moreMenuLink.getAttribute('aria-expanded') === 'true' || false;
  moreMenuLink.setAttribute('aria-expanded', !expanded);
  
  var open = secondaryNav.style.display === 'flex' || false;
  secondaryNav.style.display = open ? 'none' : 'flex';
  
  if (!open) {
    var firstVisibleItem = secondaryNav.querySelector('li[style="display: block;"] a');
    firstVisibleItem.focus();
  }
});
```

Let's also add two `keydown` event listeners: one on the menu link to make it behave like a `button` and open the secondary nav on `space` and one on the secondary nav to close on `esc`:

```javascript
moreMenuLink.addEventListener('keydown', function (e) {
  if (e.keyCode === 32) {
    e.preventDefault();
    moreMenuLink.click();
  }
});

secondaryNav.addEventListener('keydown', function (e) {
  if (e.keyCode === 27) {
    e.preventDefault();
    moreMenuLink.setAttribute('aria-expanded', false);
    secondaryNav.style.display = 'none';
    moreMenuLink.focus();
  }
});
```

### Adding some `IntersectionObserver`

Now has come the time for the grand finale - enter stage left the `IntersectionObserver`! 

This article assumes a level of familiarity with the concept and functionality of the API - if you're looking for an in-depth explanation I highly recommend [this really great write up by Heather Weaver](https://www.hweaver.com/intersection-observer-single-page-navigation/).

First, let's ensure its supported by the browser and turn off the `overflow-x` we had previously set on the main nav if it is. Let's also configure the settings for our observer while we're at it. Note, we are setting the `root` property of the observer to the `div` that wraps the main nav `ul`:

```javascript
if ('IntersectionObserver' in window) {
  mainNav.style.overflowX = 'hidden';
  
  var observerSettings = {
    root: mainNav,
    threshold: 0.98
  }
```

Now let's define the function that we'll pass to the observer to be called every time an intersection occurs:

```javascript
  var secondaryNavItems = secondaryNav.querySelectorAll('li');
  
  var callback = function (items, observer) {
    Array.prototype.forEach.call(items, function(item) {
      var index = parseInt(item.target.dataset.index);
      if (item.intersectionRatio > 0.98) {
        item.target.style.visibility = 'visible'; 
        secondaryNavItems[index].style.display = 'none';
      } else {
        item.target.style.visibility = 'hidden';
        secondaryNavItems[index].style.display = 'block';
      }
    });    
  };
```

We determine which item to display in the secondary nav by checking the value of the observed item's `data-index` attribute, which is its 0-index of its render order. Since both list's items are rendered in the same order, we can use its index to find the correct item in the secondary nav list. 

Finally, we initialize our observer with our settings and callback and call `observe` on each of our main nav items:

```javascript
  var mainNavItems = mainNav.querySelectorAll('li');
  
  var observer = new IntersectionObserver(callback, observerSettings);
  Array.prototype.forEach.call(mainNavItems, function(item) {
    observer.observe(item);
  });
}
```

And voila! Just like that we are finished with our observer as well as our entire responsive, hamburgerless nav bar!!

Side note: I just realized I could have been making vegetarian puns this whole time *insert facepalm emoji*

### Conclusion

I really love figuring out how things like this work, aside from the momentary fits of rage that occur when I set `flex` on the wrong element. So I hope this little article decreases the number of other folks' fits of rage and helps out anyone that is interested in building something similar.

Feel free to reach out if you have any questions or comments. & have fun!!