In this tutorial, we’ll create a reusable, custom dropdown component, using vanilla JavaScript.
A what? One strategy to reduce a menu or navigation footprint is to place additional links inside a dropdown component. The idea is that users click to reveal more options, thus giving them more ways to navigate web pages, whilst keeping the UI clean.
For this tutorial, I’ll assume we’re building a component for a marketing agency website.
How to Build a Dropdown Component With JavaScript
Alright, let’s get stuck in! As usual, we’ll begin with the HTML markup for our srtucture, then we’ll add some styles with CSS, and finally we’ll add the behavior with vanilla JavaScript.
HTML Structure
First, the HTML structure. We will use an unordered list with list items and links.
1 |
<nav role="primary navigation"> |
2 |
<a href="#">Home</a> |
3 |
<div class="dropdown"> |
4 |
<a href="#" class="dropdown-action"> |
5 |
Services |
6 |
<svg class="dropdown-icon" xmlns="https://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>chevron-down</title><g fill="none"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.25 10.75L12 14.25l-3.25-3.5"></path></g></svg> |
7 |
</a>
|
8 |
<ul class="dropdown-menu"> |
9 |
<li><a href="#">SEO</a></li> |
10 |
<li><a href="#">Content Strategy</a></li> |
11 |
<li><a href="#">Copywriting</a></li> |
12 |
<li><a href="#">Storytelling</a></li> |
13 |
</ul>
|
14 |
</div>
|
15 |
<a href="#">About</a> |
16 |
<a href="#">Contact</a> |
17 |
<div class="dropdown"> |
18 |
<a href="#" class="dropdown-action"> |
19 |
Account |
20 |
<svg class="dropdown-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>chevron-down</title><g fill="none"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.25 10.75L12 14.25l-3.25-3.5"></path></g></svg> |
21 |
</a>
|
22 |
<ul class="dropdown-menu"> |
23 |
<li><a href="#">Login</a></li> |
24 |
<li><a href="#">Sign up</a></li> |
25 |
</ul>
|
26 |
</div>
|
27 |
</nav>
|
Note the class names on the parent anchor link .dropdown-action
and the unordered list .dropdown-menu
. These class naming conventions are more generic, so we can reuse the CSS and JavaScript as often as we wish on the website.
Adding Some Styles With CSS
Let’s give the dropdown list some aesthetic treatments with CSS.
Remember that a core foundation of a dropdown is appropriately hiding and showing the list on demand. I’ll design the dropdown list with that in mind.
1 |
nav { |
2 |
width: 1020px; |
3 |
display: flex; |
4 |
align-items: center; |
5 |
margin: 20px auto; |
6 |
padding-bottom: 10px; |
7 |
border-bottom: 1px solid #ddd; |
8 |
}
|
9 |
|
10 |
nav a { |
11 |
padding: 10px 16px; |
12 |
border-radius: 4px; |
13 |
display: inline-block; |
14 |
color: #334155; |
15 |
text-decoration: none; |
16 |
}
|
17 |
|
18 |
nav a:hover { |
19 |
color: #0ea5e9; |
20 |
background-color: #f0f9ff; |
21 |
}
|
22 |
|
23 |
.dropdown { |
24 |
position: relative; |
25 |
}
|
26 |
|
27 |
.dropdown-action { |
28 |
display: flex; |
29 |
align-items: center; |
30 |
padding-right: 10px; |
31 |
}
|
32 |
|
33 |
.dropdown-icon { |
34 |
stroke: currentColor; |
35 |
}
|
36 |
|
37 |
.dropdown-menu { |
38 |
display: none; |
39 |
list-style: none; |
40 |
margin: 0; |
41 |
padding: 10px 0; |
42 |
border: 1px solid #cbd5e1; |
43 |
border-radius: 6px; |
44 |
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
45 |
position: absolute; |
46 |
top: 40px; |
47 |
left: 5px; |
48 |
min-width: 180px; |
49 |
background: white; |
50 |
}
|
51 |
|
52 |
.dropdown-menu.active { |
53 |
display: block; |
54 |
}
|
55 |
|
56 |
.dropdown-menu a { |
57 |
display: block; |
58 |
}
|
Adding Interactivity with JavaScript
To make sure we can reuse the dropdown component over and over, I’ll write the JavaScript in a way that will account for any dropdown components on a given page. This means all we need to do is include the script for it to work across many dropdown components assuming the HTML and CSS match our previous work.
1 |
class Dropdown { |
2 |
|
3 |
constructor() { |
4 |
this.dropdowns = document.querySelectorAll('.dropdown .dropdown-menu') |
5 |
if (this. dropdowns.length) { |
6 |
this.initialize(); |
7 |
}
|
8 |
}
|
9 |
|
10 |
|
11 |
initialize() { |
12 |
document.addEventListener('click', (e) => { |
13 |
if (e.target.classList.contains('dropdown-action')) { |
14 |
this.hideOtherDropdowns(e.target); |
15 |
this.handleClick(e.target); |
16 |
} else { |
17 |
this.hideOtherDropdowns(null); |
18 |
}
|
19 |
});
|
20 |
}
|
21 |
|
22 |
handleClick(dropdownAction) { |
23 |
this. dropdowns.forEach(dropdown => { |
24 |
if (dropdown.parentElement.contains(dropdownAction)) { |
25 |
dropdown.classList.add('active'); |
26 |
} else { |
27 |
dropdown.classList.remove('active'); |
28 |
}
|
29 |
})
|
30 |
}
|
31 |
|
32 |
hideOtherDropdowns(dropdownAction) { |
33 |
|
34 |
this.dropdowns.forEach((dropdown) => { |
35 |
if (!dropdown.parentElement.contains(dropdownAction)) { |
36 |
dropdown.classList.remove('active'); |
37 |
}
|
38 |
});
|
39 |
}
|
40 |
|
41 |
}
|
42 |
|
43 |
document.addEventListener('DOMContentLoaded', () => new Dropdown); |
Using modern ES6 syntax, I created a new Dropdown
class to wrap logic for reuse on a given page. We initialize it at the very in using the following line.
1 |
document.addEventListener('DOMContentLoaded', () => new Dropdown); |
As long as the script is included in a given page, this code is all you need to make the component come alive. We can wait for the DOM content to be loaded before initializing the Dropdown
class for better results.
Our Functions (in More Detail)
Let’s break apart each function and dive deeper into what is happening.
The Constructor Method
1 |
constructor() { |
2 |
this.dropdowns = document.querySelectorAll('.dropdown .dropdown-menu') |
3 |
if (this. dropdowns.length) { |
4 |
this.initialize(); |
5 |
}
|
6 |
}
|
The constructor
method is used to initialize the object or class. A constructor enables you to provide any custom initialization before other methods can be called.
Here I added a querySelectorAll
method that queries for all dropdown menus on a given page. We’ll loop through those in the coming functions.
Additionally, I added some conditional logic to check that dropdowns exist on a page before calling the initialize()
function that is part of the class.
The initialize() Function
1 |
initialize() { |
2 |
document.addEventListener('click', (e) => { |
3 |
if (e.target.classList.contains('dropdown-action')) { |
4 |
this.hideOtherDropdowns(e.target); |
5 |
this.handleClick(e.target); |
6 |
} else { |
7 |
this.hideOtherDropdowns(null); |
8 |
}
|
9 |
});
|
10 |
}
|
The initialize()
function calls the other functions inside the class. Here I added an event listener to the entire HTML document. We need to do this to respond to a user’s action when they click outside of a given dropdown menu. A good experience is automatically closing the menu when clicking outside of it, so this logic accounts for that.
We can tap into the “click” by using the event that gets returned, and tapping into it for future use. The idea is to use the item the user clicked on to pave the way for showing and hiding the correct menus on the page. If there are multiple menus, we’ll want to close the menus not actively in use automatically.
If the navigation menu link contains the dropdown-action
class, we will call two other functions and pass the same target value through to those. Otherwise, we’ll close all other dropdowns using the hideOtherDropdowns()
function.
The handleClick() Function
1 |
handleClick(dropdownAction) { |
2 |
this. dropdowns.forEach(dropdown => { |
3 |
if (dropdown.parentElement.contains(dropdownAction)) { |
4 |
dropdown.classList.add('active'); |
5 |
} else { |
6 |
dropdown.classList.remove('active'); |
7 |
}
|
8 |
})
|
9 |
}
|
The handleClick()
function accepts the event.target
property. We pass the property from inside the initialize()
function. We’ll leverage our previously initialized this.dropdowns
array to loop through all dropdowns that exist. If a dropdown contains the event.target
(or dropdownAction
), we add the class .active
to the dropdown menu and show it. If that isn’t the case we will remove the class .active
.
The dropdownAction Parameter
1 |
hideOtherDropdowns(dropdownAction) { |
2 |
this. dropdowns.forEach((dropdown) => { |
3 |
if (!dropdown.parentElement.contains(dropdownAction)) { |
4 |
dropdown.classList.remove('active'); |
5 |
}
|
6 |
});
|
7 |
}
|
Having fewer dropdowns open at once would make for a better experience in the long run. We need to close any that are not actively in use. We can figure out which is used relative to the event.target
being passed through from the initialize()
function. Here we use a parameter called dropdownAction
to sub in for the event.target
property.
We’ll loop through all dropdowns once more. Within the forEach
loop, we can perform a conditional statement that checks if the dropdown contains the dropdownAction
or not. We’ll use the !
to denote that we’ll check the inverse of the logic. So in plain terms, I’m checking if the dropdown does not contain the link to open the dropdown. If not, we’ll remove its active class and hide it.
Conclusion
With any luck, you can now try opening and closing both dropdowns we added in the HTML, and they should respond as we like. When one opens, the other closes. Notice also that when you click outside of a dropdown, the original dropdown menu also closes.
I hope you enjoyed following this JavaScript tutorial, and learned something you can use. Thanks for reading!