Just for fun — let's build a reusable dropdown menu component with VueJS. You can check out the working demo here.
If you just want to use the component, you can find it on npm or github
Let's build the thing 🚀
We're assuming that you have a basic understanding of how the VueJS and VueJS single file components (SFC) work and that you already have a VueJS project running
1. Create a file called src/components/vue-dropdown-menu.vue
and add following basic SFC structure:
<template>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>
As you can see — just a basic SFC structure here — nothing magical.
2. Add the following HTML markup to the <template>
part of the SFC structure
<template>
<section class="dropDownMenuWrapper">
<button class="dropDownMenuButton">
</button>
<div class="iconWrapper">
<div class="bar1" />
<div class="bar2" />
<div class="bar3" />
</div>
<section class="dropdownMenu">
<div class="menuArrow" />
<slot/>
</section>
</section>
</template>
⬆️ What's happening here:
.dropDownMenuWrapper
An element that will wrap our component
.dropDownMenuButton
A button that will actually open & close our menu
.iconWrapper
( And the .bar elements )
Pure CSS icon that indicates if the menu is open or closed
.dropdownMenu
An element that will wrap the actual menu content —links and such.
.menuArrow
Just a for pointing purposes 😁
<slot/>
Content from the parent will be printed here
3. Add styles to the <style>
part of the SFC structure
.dropDownMenuWrapper {
position: relative;
width: 500px;
height: 80px;
border-radius: 8px;
background: white;
border: 1px solid #eee;
box-shadow: 10px 10px 0 0 rgba(black,.03);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
* {
box-sizing: border-box;
text-align: left;
}
.dropDownMenuButton {
border: none;
font-size: inherit;
background: none;
outline: none;
border-radius: 4px;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
padding: 0 70px 0 20px;
margin: 0;
line-height: 1;
width: 100%;
height: 100%;
z-index: 2;
cursor: pointer;
}
.dropDownMenuButton--dark {
color: #eee;
}
.iconWrapper {
width: 25px;
height: 25px;
position: absolute;
right: 30px;
top: 50%;
transform: translate(0,-50%);
z-index: 1;
.bar1 {
width: 100%;
max-width: 28px;
height: 3px;
background: blue;
position: absolute;
top: 50%;
left: 50%;
border-radius: 9999px;
transform: translate(-50%, calc(-50% - 8px) );
transition: all 0.2s ease;
}
.bar1--dark {
background: #eee;
}
.bar1--open {
transform: translate(-50%, -50%) rotate(45deg);
margin-top: 0;
background: red;
}
.bar2 {
width: 100%;
max-width: 28px;
height: 3px;
background: blue;
position: absolute;
top: 50%;
left: 50%;
border-radius: 9999px;
opacity: 1;
transform: translate(-50%, -50%);
transition: all 0.2s ease;
}
.bar2--dark {
background: #eee;
}
.bar2--open {
opacity: 0;
}
.bar3 {
width: 100%;
max-width: 28px;
height: 3px;
background: blue;
position: absolute;
top: 50%;
left: 50%;
border-radius: 9999px;
transform: translate(-50%, calc(-50% + 8px) );
transition: all 0.2s ease;
}
.bar3--dark {
background: #eee;
}
.bar3--open {
top: 50%;
transform: translate(-50%, -50% ) rotate(-45deg);
background: red;
}
}
.iconWrapper--noTitle {
left: 0;
top: 0;
bottom: 0;
right: 0;
width: auto;
height: auto;
transform: none;
}
.dropdownMenu {
position: absolute;
top: 100%;
width: 100%;
min-width: 300px;
min-height: 10px;
border-radius: 8px;
border: 1px solid #eee;
box-shadow: 10px 10px 0 0 rgba(black,.03);
background: white;
padding: 10px 30px;
animation: menu 0.3s ease forwards;
.menuArrow {
width: 20px;
height: 20px;
position: absolute;
top: -10px;
left: 20px;
border-left: 1px solid #eee;
border-top: 1px solid #eee;
background: white;
transform: rotate(45deg);
border-radius: 4px 0 0 0;
}
.menuArrow--dark {
background: #333;
border: none;
}
.option {
width: 100%;
border-bottom: 1px solid #eee;
padding: 20px 0;
cursor: pointer;
position: relative;
z-index: 2;
&:last-child {
border-bottom: 0;
}
* {
color: inherit;
text-decoration: none;
background: none;
border: 0;
padding: 0;
outline: none;
cursor: pointer;
}
}
.desc {
opacity: 0.5;
display: block;
width: 100%;
font-size: 14px;
margin: 3px 0 0 0;
cursor: default;
}
}
.dropdownMenu--dark {
background: #333;
border: none;
.option {
border-bottom: 1px solid #888;
}
* {
color: #eee;
}
}
@keyframes menu {
from { transform: translate3d( 0, 30px ,0 ) }
to { transform: translate3d( 0, 20px ,0 ) }
}
}
.dropDownMenuWrapper--noTitle {
padding: 0;
width: 60px;
height: 60px;
}
.dropDownMenuWrapper--dark {
background: #333;
border: none;
}
Pretty basic styling — We are not going thru all these — as you can style your component anyway you like.
4. Add some function to our component.
Previously we added the .dropDownMenuButton
-button to the template, and now we are going expand that element to actually do something. Modify the element as follows:
<button class="dropDownMenuButton" ref="menu" @click="openClose">{{menuTitle}}</button>
⬆️ What's happening here:
- We added the
@click="openClose"
which will fire the methodopenClose
when we click the button. - We added the
ref="menu"
that refers to the element — we need this later on. - We added the template tag
{{menuTitle}}
that will show us our menu title.
— then, let's create the openClose
method to control the opening and closing the menu. So modify the <script>
part of the structure like this:
export default {
props: [ "menuTitle" ], // Menu title from the parent
data() {
return {
isOpen: false // Variable if the menu is open or closed
},
methods: {
openClose() {
// Toggle between open or closed ( true || false )
isOpen = !isOpen
}
}
}
⬆️ What's happening here:
We added the openClose
method to toggle isOpen
variable between true and false — we also added the menuTitle
prop so we can pass our menus title from the parent.
— to make things actually work, we need to add the isOpen
variable to the template:
Modify the .bar1
& .bar2
& .bar3
elements as follows:
<div class="bar1" :class="{ 'bar1--open' : isOpen }" />
<div class="bar2" :class="{ 'bar2--open' : isOpen }" />
<div class="bar3" :class="{ 'bar3--open' : isOpen }" />
Also modify the .dropdownMenu
as follows:
<section class="dropdownMenu" v-if="isOpen" >
<div class="menuArrow" />
<slot/>
</section>
⬆️ What's happening here:
We added the :class="{ 'bar1--open' : isOpen }"
to the bar -elements — we toggle classes based on the value of isOpen
so we can get that nice icon animation that you can see in the demo.
In the .dropdownMenu
-element we added the v-if="isOpen"
part — if isOpen
is true show the menu and vice versa.
Congrats 🏆
You now have a working component! BUT... We can make it even better. For the UI/UX purposes — we should add a function that closes the menu if the user clicks anywhere else on the document. To add that, we have to expand the openClose
method and add a new method called catchOutsideClick
.
First let's expand the openClose
method, modify the method to look like this:
openClose() { var _this = this
const closeListerner = (e) => {
if ( _this.catchOutsideClick(e, _this.$refs.menu ) )
window.removeEventListener('click', closeListerner) , _this.isOpen = false
}
window.addEventListener('click', closeListerner)
this.isOpen = !this.isOpen
},
— then we need to add a new method called catchOutsideClick
;
catchOutsideClick(event, dropdown) {
// When user clicks menu — do nothing
if( dropdown == event.target )
return false
// When user clicks outside of the menu — close the menu
if( this.isOpen && dropdown != event.target )
return true
}
⬆️ What's happening here:
We added an eventListener to catch all click
events — when we catch one, we pass the event and the clicked element to catchOutsideClick
method which will then check if the click is on the menu or outside of it. If the menu is open and the click was outside the menu — we will remove the eventListener and close the menu.
Bonus 🎉
You might have noticed earlier — that we have a bunch of --dark
classes in the styles. That's because we want our component to support a dark mode if the user prefers that.
So to make those styles work we are adding a bit more code to our component.
First, let's make our template to look like this:
<section class="dropDownMenuWrapper" :class="{ 'dropDownMenuWrapper--dark' : isDarkMode, 'dropDownMenuWrapper--noTitle' : !menuTitle }">
<button class="dropDownMenuButton" ref="menu" @click="openClose" :class="{ 'dropDownMenuButton--dark' : isDarkMode }">
{{ menuTitle }}
</button>
<div class="iconWrapper" :class="{ 'iconWrapper--noTitle' : !menuTitle }">
<div class="bar1" :class="{ 'bar1--open' : isOpen , 'bar1--dark' : isDarkMode }" />
<div class="bar2" :class="{ 'bar2--open' : isOpen , 'bar2--dark' : isDarkMode }" />
<div class="bar3" :class="{ 'bar3--open' : isOpen , 'bar3--dark' : isDarkMode }" />
</div>
<section class="dropdownMenu" v-if="isOpen" :class="{ 'dropdownMenu--dark' : isDarkMode }">
<div class="menuArrow" :class="{ 'menuArrow--dark' : isDarkMode }" />
<slot/>
</section>
</section>
Second, add new variable called isDarkMode
and prop
called darkMode
:
props: [ "darkMode", "menuTitle" ],
data() {
return {
isOpen: false,
isDarkMode: false
}
}
Third, add watcher to watch darkMode
prop:
watch: {
darkMode(val) {
// Force dark mode
if( !val )
this.isDarkMode = false
// Force dark mode
if( val == 'force' )
this.isDarkMode = true
// Switch dark / light mode automatically according to what user prefer
if( val == 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches )
this.isDarkMode = true
}
}
Whats happening here ⬆️:
We added a new prop and variable to indicate what style we want to use — we also added conditional classes to all HTML elements — so that if isDarkMode
is true we add a special --dark
class to elements and lastly we added a watcher to change the mode accordingly.
darkMode
prop accepts three kinds of values:
false
→ Always show light mode
force
→ Always show dark mode
auto
→ Automatically change according to what user prefs
You can find the whole code for the component here
How to use
- Include the component
- Use it
<dropdown-menu menu-title="Vue Dropdown Menu" dark-mode="auto">
<section class="option">
<button @click="sayHello">This is button for method</button>
<span class="desc">This is Vue dropdown menu method that says hello for you.</span>
</section>
<section class="option">
<a href="https://duckduckgo.com">This is basic a -link</a>
<span class="desc">Clicking this takes you somewhere else.</span>
</section>
<section class="option">
<router-link to="/about">This is Vue router link</router-link>
<span class="desc">Clicking this takes you somewhere else.</span>
</section>
</dropdown-menu>
🎉✌️🙏
Top comments (0)