Codelab: Building a Sidenav component

This codelab teaches you how to build a responsive slide out side navigation layout component on the web. We'll build the component as we go, starting with HTML, then CSS, then JavaScript.

Check out my blog post Building a Sidenav component to learn about the CSS web platform features chosen for building this component.

Setup #

  1. Click Remix to Edit to make the project editable.
  2. Open app/index.html.

HTML #

First, get the essentials of the HTML setup so there's content and some boxes to work with.

Drop the following HTML into the <body> tag.

<aside></aside>
<main></main>

The <aside> holds the navigation menu as a complimentary element to <main>, which holds the primary page content.

Next we'll fill in those semantic elements with the rest of the page content.

Add a navigation element, some navigation links and a close link inside the <aside> element.

<aside>
<nav>
<h4>My</h4>
<a href="#">Dashboard</a>
<a href="#">Profile</a>
<a href="#">Preferences</a>
<a href="#">Archive</a>

<h4>Settings</h4>
<a href="#">Accessibility</a>
<a href="#">Theme</a>
<a href="#">Admin</a>
</nav>

<a href="#"></a>
</aside>

Links go great inside <nav> elements, and <nav> elements go great in <aside> sidebars. Still, there's more we can do to improve.

In the main content element, add a header and an article to semantically hold the layout content.

<main>
<header>
<a href="#sidenav-open" class="hamburger">
<svg viewBox="0 0 50 40">
<line x1="0" x2="100%" y1="10%" y2="10%" />
<line x1="0" x2="100%" y1="50%" y2="50%" />
<line x1="0" x2="100%" y1="90%" y2="90%" />
</svg>
</a>
<h1>Site Title</h1>
</header>

<article>
{put some placeholder content here}
</article>
</main>

You can ignore the The attribute name of [ viewBox ] must be in lowercase lint error from Glitch.

The header has the menu open link. The aside has the close button. We'll show and hide elements based on viewport size soon.

In the <article> element, we pasted a placeholder sentence. Replace `` with your own stuff, or paste the lorem provided below:

<h2>Totam Header</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Cum consectetur, necessitatibus velit officia ut impedit veritatis temporibus soluta? Totam odit cupiditate facilis nisi sunt hic necessitatibus voluptatem nihil doloribus! Enim.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>

<h3>Subhead Totam Odit</h3>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>

<h3>Subhead</h3>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>
<p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fugit rerum, amet odio explicabo voluptas eos cum libero, ex esse quasi optio incidunt soluta eligendi labore error corrupti! Dolore, cupiditate porro.</p>

This content, and it's length, are what will cause the page to be scrollable when it exceeds your viewport height.

So far you've added an aside element, with a nav, links and a way to close the sidenav. You also added a header, a way to open the sidenav, and an article to the main element. This is clean, semantic and pretty timeless already, but we can make it cleaner and clearer for everyone. The open link in the sidenav could be more clearly marked.

Add attributes title and aria-label to the header open link element:

<a href="#sidenav-open" class="hamburger">
<a href="#sidenav-open" title="Open Menu" aria-label="Open Menu" class="hamburger">

The open SVG icon could be more clearly marked as well. Add the following attributes to the SVG inside the open link element:

<svg viewBox="0 0 50 40">
<svg viewBox="0 0 50 40" role="presentation" focusable="false" aria-label="trigram for heaven symbol">

The close link in the sidenav could be more clearly marked. Add attributes title and aria-label to the sidenav close link element:

<a href="#"></a>
<a href="#" title="Close Menu" aria-label="Close Menu"></a>
Gotchas!

While all links do not require titles or special labels, I believe these to be an exception since they're being used to change the URL, like a link does, but they don't have any text content of their own.

CSS #

Time to layout the elements. The main content and sidenav are direct children of the <body> tag, so that's a good place to start.

Add the following CSS into css/sidenav.css so the <body> element lays out the children.

body {
display: grid;
grid: [stack] 1fr / min-content [stack] 1fr;

@media (max-width: 540px) {
& > :matches(aside, main) {
grid-area: stack;
}
}
}

This layout essentially says: Create a named row stack with everything in it, and 2 columns in that row, the 2nd of which is named stack also. The 1st column should be sized by its minimal content needs, and the 2nd column can take up the rest. Then, if in a constrained viewport of 540px or less, put the sidenav and main content elements into the same row and column, resulting in them being on top of each other in a 1x1 grid.

With this responsive stacking functionality as a base, we can now leverage the state of the URL bar to toggle the visibility and transition style of the sidenav.

Update the <aside> element back in app/index.html:

<aside>
<aside id="sidenav-open">

This enables CSS to match an element and the URL hash together. This is important for :target usage. Now the element's ID can match the URL hash we'll be setting with <a> tags.

In addition, for easier JavaScript targeting, add IDs for key elements that control the sidenav. First, add an ID to the sidenav open link:

<a href="#sidenav-open" class="hamburger" title="Open Menu" aria-label="Open Menu">
<a href="#sidenav-open" id="sidenav-button" class="hamburger" title="Open Menu" aria-label="Open Menu">

Next, add an ID to the sidenav close link:

<a href="#" title="Close Menu" aria-label="Close Menu"></a>
<a href="#" id="sidenav-close" title="Close Menu" aria-label="Close Menu"></a>

That wraps up the macro <body> responsive stacking layout, plus ties us into the URL bar. Let's keep going!

The <aside> has a neat layout too. It has 2 children, a <nav> which is the paper-like looking component that slides out, and a closing <a> link element that sets the url has to #. The link is invisible to the right of the paper slide out nav; it's so folks can "click off" the visual component to dismiss it.

Add the following CSS to css/sidenav.css:

#sidenav-open {
display: grid;
grid-template-columns: [nav] 2fr [escape] 1fr;
}

I thought the ratio and names were a really nice touch here, where grid could shine and give a designer a lot of control.

Next I need to conditionally overlay the main content and persist my position through any document scrolling. This is a great job for position: sticky and some overscroll-behavior.

Add the following styles for the sidenav:

#sidenav-open {
display: grid;
grid-template-columns: [nav] 2fr [escape] 1fr;

@media (max-width: 540px) {
position: sticky;
top: 0;
max-height: 100vh;
overflow: hidden auto;
overscroll-behavior: contain;

visibility: hidden; /* not keyboard accessible when closed */
}
}

Those styles ensure the sidenav is the viewport height, scrolls vertically and contains the scroll. Very importantly, it hides the element. By default, when the viewport is 540px or smaller, hide that sidenav. Unless!

Add a :target pseudo-selector to the #sidenav-open element:

#sidenav-open {

@media (max-width: 540px) {

&:target {
visibility: visible;
}
}
}

When the ID of that element and the URL bar are the same, set visibility to visible. Go ahead and open the side menu after scrolling the page, or try scrolling the page while the sidenav is open. What do you think?

Add the following CSS to the bottom of app/sidenav.css:

#sidenav-button,
#sidenav-close
{
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
user-select: none;
touch-action: manipulation;

@media (min-width: 540px) {
display: none;
}
}

These styles target our open and close buttons, specifies their tap and touch styles, and also hides them when viewports are 540px or larger.

For some flair, let's add CSS transforms with respectful accessibility. Add the following CSS to css/sidenav.css:

#sidenav-open {
--easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1);
--duration: .6s;

...

@media (max-width: 540px) {
...

transform: translateX(-110vw);
will-change: transform;
transition:
transform var(--duration) var(--easeOutExpo),
visibility 0s linear var(--duration);

&:target {
visibility: visible;
transform: translateX(0);
transition: transform var(--duration) var(--easeOutExpo);
}
}

@media (prefers-reduced-motion: reduce) {
--duration: 1ms;
}
}
A demo of the interaction with and without duration applied based on the `prefers-reduced-motion` media query.

Sprinkle in some JavaScript #

The Escape key should close the menu. Add this JS to js/index.js:

const sidenav = document.querySelector('#sidenav-open');

sidenav.addEventListener('keyup', e => {
if (e.code === 'Escape') {
document.location.hash = '';
}
});

This listens for a key event on the sidenav element. If it's Escape, it sets the URL hash to empty, making the sidenav transition out.

This next piece of UX JS is focus management. I want to make opening and closing easy, so I wait until the sidenav has finished a transition of some kind, then cross check it against the URL hash to determine if it's in or out. I then use JavaScript to set the focus on the button complimentary to the one they just pressed.

Add the following JavaScript to js/index.js:

const closenav = document.querySelector('#sidenav-close');
const opennav = document.querySelector('#sidenav-button');

sidenav.addEventListener('transitionend', e => {
if (e.propertyName !== 'transform') {
return;
}

const isOpen = document.location.hash === '#sidenav-open';

isOpen
? closenav.focus()
: opennav.focus();
});

Try it out #

  • To preview the site, press View App. Then press Fullscreen fullscreen.

Conclusion #

That's a wrap up for the needs I had with the component. Feel free to build upon it, drive it with JavaScript state instead of the URL, and in general make it yours! There's always more to add or more use cases to cover.

Open up css/brandnav.css to check out the non-layout related styles that I applied to this component. I didn't feel it was important to the feature set I was focusing on, and I hoped that separating styles from layout would encourage copy and paste. There could be more learning for you there!

How do you make slide out responsive sidenav components? Do you ever have more than 1, like one on both sides? I'd love to feature your solution in a YouTube video, make sure to tweet at me or comment in YouTube with your code, it'll help everyone out!

Last updated: Improve article