How to close a navigation menu when the page is clicked
How to close a navigation menu when the page is clicked
This article assumes you already have a working navigation menu on your website that can be opened and closed with a button click (or something similar).
A brief context to the problem
I am working on a UI that uses only HTML, CSS and JavaScript (no jQuery allowed). After creating a navigation menu that collapses on small screens, I needed a way to toggle it. Obviously, I had to have some form of ‘buttons’ for that to work, which I added and it works fine. The problem is, I want the menu to close when the user clicks outside, not just on the ‘close’ button.
Why you ask? Well, it makes for a better user experience, especially on modern smartphones that are too tall for the average user to reach a menu button. Also, it feels more natural.
The obvious fix
At first, anyone would think ‘why not just add an event listener to the body’, and they will be correct. So, the event fires every time, checks if the menu is open and closes it. While this may seem like it will work right away, it won’t work as expected. The event will fire when any part of the menu is clicked, which is not what we want. In the next sections, I will list the steps involved in achieving the desired result (bear in mind that there are several ways of doing this
Step 1: Place the event listener on the HTML node, not the body
The subtle difference is observed when the body doesn’t cover the entire height/width of the device. In this case, it makes more sense to use the HTML node, which will always be the entire browser window.
Step 2: Only add the listener when the menu is open
Adding an event listener on page load is not quite intuitive as the event will be fired every single time, even when it is not needed. Another reason is it may interfere with other listeners on the page. I will be passing a ‘named’ function to the event listener as opposed to using an anonymous callback function. I will explain why in the later sections, just assume we have the function already.
const html = document.documentElement;const menu = document.getElementById('myMenuId');
function openMenu(){ // add class to the menu to make it show menu.classList.add('animate'); // add event listener to the html element html.addEventListener('click', closeMenuOnBodyClick);}
Step 3: Remove the listener when the menu is closed
function closeMenu(){ // add class to the menu to make it show menu.classList.remove('animate'); // add event listener to the html element html.removeEventListener('click', closeMenuOnBodyClick);}
Step 4: Write the closeMenuOnBodyClick() function
This function basically calls the closeMenu()
function when the html element is clicked.
function closeMenuOnBodyClick(event){ closeMenu();}
Here comes the tricky part. We have to find a way to check if any part of the menu was clicked so as to not call the closeMenu()
function. A simple way to do this is to check ‘path’ property of the event, which is an array of all the event.target
ancestors (this is different from event.currentTarget
, more on this here), whether the menu element is present. This is done by calling the composedPath()
method of the event. If this is not clear enough, that’s understandable, it is quite confusing. Here is a nice article on how events work. Let’s add this logic to the function.
function closeMenuOnBodyClick(event){ // get the event path const path = event.composedPath(); // check if it has the menu element if (path.some(elem => elem.id === 'myMenuId')) { // terminate this function if it does return; } closeMenu();}
Here, we use the built-in Array.some()
method to check if any of the elements in the array has an id that is equal to myMenuId
, and then break out of the function. This way, closeMenu()
is never called.
You will also notice that I’m using the event
parameter on the named function, even though it is outside the event listener. This is possible due to the fact that addEventListener
always passes the event as the first argument to the callback function, whether named or anonymous.
I promised to explain why I used a named function. We want to be able to remove the event listener once the menu is closed and this is done by calling removeEventListener(type, function)
with the type and function as the first two parameters, which have to be the exact same ones we used when adding the listener. This cannot be done with an anonymous function.
TL,DR
To summarize:
Add a click event listener to the HTML element when the menu is open, when the page is clicked, check the event path to see if any part of the menu was clicked and do nothing, close the menu otherwise, remove the event listener from the page when the menu is closed.
Please comment below if you need clarification on any areas, corrections and/or suggestions to even make this process simpler or more efficient.