How to Build a Responsive Off-Canvas Menu With CSS and a Touch of JavaScript
In this tutorial, we’ll go through a simple yet effective method for creating a responsive off-canvas menu with HTML, CSS, and JavaScript.
To get an initial idea of what we’ll be building, take a look at the related CodePen demo (check out the larger version to see how the layout changes):
1. Begin With Markup
Our markup will consist of two wrapper elements:
- the
.top-banner
element - the
.top-nav
element
Here’s the HTML code:
1 |
<section class="top-banner"> |
2 |
<div class="top-banner-overlay"> |
3 |
<!-- content here -->
|
4 |
</div>
|
5 |
</section>
|
6 |
|
7 |
<nav class="top-nav"> |
8 |
<div class="menu-wrapper"> |
9 |
<!-- content here -->
|
10 |
</div>
|
11 |
<div class="fixed-menu"> |
12 |
<!-- content here -->
|
13 |
</div>
|
14 |
</nav>
|
2. Next We Need Some CSS
With the markup ready, next let’s examine the most important styles which are required for our menu.
Styling the top-banner Element
The .top-banner
element will look like this:



Regarding its styles, we'll do the following:
- Set its width equal to the window width minus the width of the
.fixed-menu
element. - Set its height equal to the window height.
- Define it as a flex container. This will force its overlay to cover the full parent height.
- Use flexbox to vertically center the overlay’s content.
Here are the styles we’ll need to achieve all that:
1 |
/*CUSTOM VARIABLES HERE*/
|
2 |
|
3 |
.top-banner { |
4 |
position: relative; |
5 |
left: 150px; |
6 |
display: flex; |
7 |
width: calc(100% - 150px); |
8 |
height: 100vh; |
9 |
background: url(IMAGE_PATH) no-repeat center / cover; |
10 |
}
|
11 |
|
12 |
.top-banner-overlay { |
13 |
display: flex; |
14 |
flex-direction: column; |
15 |
justify-content: center; |
16 |
width: 350px; |
17 |
padding: 20px; |
18 |
transition: all .7s; |
19 |
color: var(--white); |
20 |
background: rgba(0, 0, 0, .7); |
21 |
}
|
Styling the .top-nav
Element
The .top-nav
element will look like this:



In this case, we’ll do the following:
- Specify the direct child elements as fixed positioned elements that will cover the window height.
- Use flexbox to vertically align the
.fixed-menu
element. - Hide the
.menu-wrapper
element by default. To do so, we won't give it a property value such asdisplay: none
. In fact, we'll use thetranslate()
function to move it 200px to the left. Keep in mind that the element’s width is 350px, so part of it will still be within the viewport. However, it won't be visible because the element is positioned underneath the.fixed-menu
element. - Hide the menu list.
Have a look at the corresponding CSS styles below:
1 |
/*CUSTOM VARIABLES HERE*/
|
2 |
|
3 |
.top-nav .menu-wrapper { |
4 |
position: fixed; |
5 |
top: 0; |
6 |
left: 0; |
7 |
bottom: 0; |
8 |
width: 350px; |
9 |
padding: 20px; |
10 |
transform: translateX(-200px); |
11 |
transition: transform .7s; |
12 |
background: var(--menu-color); |
13 |
}
|
14 |
|
15 |
.top-nav .menu-wrapper .menu { |
16 |
opacity: 0; |
17 |
transition: opacity .4s; |
18 |
}
|
19 |
|
20 |
.top-nav .fixed-menu { |
21 |
position: fixed; |
22 |
top: 0; |
23 |
left: 0; |
24 |
bottom: 0; |
25 |
display: flex; |
26 |
flex-direction: column; |
27 |
width: 150px; |
28 |
padding: 20px; |
29 |
background: var(--fixed-menu-color); |
30 |
}
|
3. Now for Some JavaScript
At this point, we’ll use some straightforward JavaScript code to manipulate the state of the off-canvas menu.
Toggle Menu
We can toggle the menu in the following ways:
- Each time we click on the
.menu-toggle
button, the menu’s visibility will change. If it’s hidden, it’ll appear with a nice slide-in effect, and the overlay will be pushed simultaneously to the right. Optionally, we can do a lot more things during this state. In our example, we'll add a box shadow to the::before
pseudo-element of the overlay and reveal the menu list using a fade-in effect. On the contrary, if the menu is visible, it'll disappear with a slide-out effect, and the overlay will be pushed simultaneously to the left.
- Another possible way to close the menu is through the
.menu-close
button that is part of the.menu-wrapper
element. - By default, we can also toggle the menu through the keyboard by focusing the toggle buttons and pressing the
Enter
orTab
keys. We'll also add a scenario for closing the menu by pressing theEsc
key.
Here’s the JavaScript code which implements this behavior:
1 |
const topNav = document.querySelector(".top-nav"); |
2 |
const menuToggle = topNav.querySelector(".menu-toggle"); |
3 |
const menuClose = topNav.querySelector(".menu-close"); |
4 |
const menuWrapper = topNav.querySelector(".menu-wrapper"); |
5 |
const topBannerOverlay = document.querySelector(".top-banner-overlay"); |
6 |
const isOpenedClass = "is-opened"; |
7 |
const isMovedClass = "is-moved"; |
8 |
|
9 |
menuToggle.addEventListener("click", () => { |
10 |
menuWrapper.classList.toggle(isOpenedClass); |
11 |
topBannerOverlay.classList.toggle(isMovedClass); |
12 |
});
|
13 |
|
14 |
menuClose.addEventListener("click", () => { |
15 |
menuWrapper.classList.remove(isOpenedClass); |
16 |
topBannerOverlay.classList.remove(isMovedClass); |
17 |
});
|
18 |
|
19 |
document.addEventListener("keydown", (e) => { |
20 |
if (e.key == 'Escape' && menuWrapper.classList.contains(isOpenedClass)) { |
21 |
menuClose.click(); |
22 |
}
|
23 |
});
|
And below you’ll find the associated CSS styles:
1 |
.top-banner-overlay.is-moved { |
2 |
transform: translateX(350px); |
3 |
}
|
4 |
|
5 |
.top-banner-overlay.is-moved::before { |
6 |
content: ""; |
7 |
position: absolute; |
8 |
top: 0; |
9 |
bottom: 0; |
10 |
right: 100%; |
11 |
width: 20px; |
12 |
box-shadow: 3px 0 10px rgba(0, 0, 0, .75); |
13 |
}
|
14 |
|
15 |
.top-nav .menu-wrapper.is-opened { |
16 |
transform: translateX(150px); |
17 |
}
|
18 |
|
19 |
.top-nav .menu-wrapper.is-opened .menu { |
20 |
opacity: 1; |
21 |
transition-delay: .6s; |
22 |
}
|
4. Going Responsive
At this point, we'll examine how to make our off-canvas responsive.
When the viewport is up to 900px:
- The top banner will have a height equal to the window height minus the height of the
.fixed-menu
element. - The top banner overlay will stop having the window height—it'll be vertically centered inside the banner.
- Also, the fixed header will stop having the window height. In fact, it'll have a fixed height of 50px.
- The navigation layout will switch to horizontal.
- The menu will be full-screen and continue to be off-screen by default.
- Upon menu toggle click, the banner overlay will be moved off-screen to the right.



Alternatively, keeping the banner overlay always on the screen and overlapping the menu while in view is also an option. Comment out the properties below marked with the number 2 and run the pen to see the difference.
As we’re using a desktop-first approach, these are the most important styles that we have to add/overwrite:
1 |
@media (max-width: 900px) { |
2 |
body { |
3 |
font-size: 16px; |
4 |
overflow-x: hidden; |
5 |
}
|
6 |
|
7 |
.top-banner { |
8 |
top: 50px; |
9 |
left: auto; |
10 |
width: 100%; |
11 |
height: calc(100vh - 50px); |
12 |
transform: none; |
13 |
align-items: center; |
14 |
}
|
15 |
|
16 |
.top-banner-overlay { |
17 |
position: relative; /*2*/ |
18 |
left: 0; /*2*/ |
19 |
max-width: 100%; |
20 |
}
|
21 |
|
22 |
.top-banner-overlay.is-moved { |
23 |
left: 100%; /*2*/ |
24 |
transform: none; |
25 |
}
|
26 |
|
27 |
.top-nav .menu-wrapper { |
28 |
width: 100%; |
29 |
transform: translateX(-100%); |
30 |
padding-top: 70px; |
31 |
}
|
32 |
|
33 |
.top-nav .menu-wrapper.is-opened { |
34 |
transform: none; |
35 |
}
|
36 |
|
37 |
.top-nav .menu-wrapper .menu-close { |
38 |
top: 70px; |
39 |
}
|
40 |
|
41 |
.top-nav .fixed-menu { |
42 |
bottom: auto; |
43 |
flex-direction: row; |
44 |
align-items: center; |
45 |
width: 100%; |
46 |
height: 50px; |
47 |
padding: 0 20px; |
48 |
}
|
49 |
|
50 |
.top-nav .fixed-menu .menu-toggle { |
51 |
margin: 0; |
52 |
order: 3; |
53 |
}
|
54 |
|
55 |
.top-nav .socials { |
56 |
display: flex; |
57 |
margin-left: auto; |
58 |
margin-right: 20px; |
59 |
}
|
60 |
}
|
5. Clear Transitions on Window Resize
We're almost done! Similar to what we've done in another off-canvas tutorial, let's make one more adjustment: we'll clear all transitions each time we resize the browser window. This way, during the resizing phase, we'll avoid seeing the off-canvas for a moment before moving back to its default off-screen position.
Here's the extra code that we'll need:
1 |
const body = document.body; |
2 |
const noTransitionClass = "no-transition"; |
3 |
let resize; |
4 |
|
5 |
window.addEventListener("resize", () => { |
6 |
body.classList.add(noTransitionClass); |
7 |
clearTimeout(resize); |
8 |
resize = setTimeout(() => { |
9 |
body.classList.remove(noTransitionClass); |
10 |
}, 500); |
11 |
});
|
And the related style:
1 |
.no-transition * { |
2 |
transition: none !important; |
3 |
}
|
Conclusion
That’s it, folks! We managed to build a useful off-canvas menu that will appear nicely on different screens with relatively straightforward code. You can build on the demo and make it more accessible by adding extra ARIA attributes like the aria-labelledby
attribute and the dialog
role.
I hope you enjoyed the final result and you’ll use it as inspiration for creating even more powerful off-canvas menus. And of course, if you build any, don’t forget to share them with us!
Learn More Off-Canvas Techniques
- How to Build an Off-Canvas Navigation With CSS GridIan Yates15 Feb 2017
- How to Build an Off-Canvas Navigation With jQuery.mmenuGeorge Martsoukos28 Oct 2015
- How to Build an Off-Canvas Navigation Layout With BootstrapThoriq Firdaus25 Aug 2014
- Quick Tip: Don’t Forget the Viewport Meta TagIan Yates26 Jun 2013