Building a VueJS dropdown menu component
Tags: vue, javascript, tutorial, webdev
Published: 2021-01-28 14:03:09

Just for fun — let's build a reusable dropdown menu component with VueJS. You can check out the working [demo here.](https://vuejs-dropdown-menu-demo.onrender.com/)

If you just want to use the component, you can find it on [npm](https://www.npmjs.com/package/@mmmrks/vue-dropdown-menu) or [github](https://github.com/mmmrks/vue-dropdown-menu)

&nbsp;

###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.

&nbsp;

**2. Add the following HTML markup to the** `<template>` **part of the SFC structure**

```html
<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

&nbsp;

**3. Add styles to the** `<style>` **part of the SFC structure**


```css
.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.

&nbsp;

**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:

```html
<button class="dropDownMenuButton" ref="menu" @click="openClose">{{menuTitle}}</button>
```
###⬆️ What's happening here:

1. We added the `@click="openClose"` which will fire the method `openClose` when we click the button.
2. We added the `ref="menu"` that refers to the element — we need this later on.
3. We added the template tag `{{menuTitle}}` that will show us our menu title.

&nbsp;

— 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:

```javascript
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:
```html
<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:
```html
<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:

```javascript
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

},
```
&nbsp;
— then we need to add a new method called `catchOutsideClick`;

```javascript
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:

```html
<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>
```

&nbsp;
Second, add new variable called `isDarkMode` and `prop` called `darkMode`:

```javascript
props: [ "darkMode", "menuTitle" ],
data() {
  return {
    isOpen: false,
    isDarkMode: false
  }
}
```

&nbsp;
Third, add watcher to watch `darkMode` prop:

```javascript
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` &rarr; Always show light mode
`force` &rarr; Always show dark mode 
`auto` &rarr; Automatically change according to what user prefs

You can find the whole code for the component [here](https://github.com/mmmrks/vue-dropdown-menu/blob/master/src/vue-dropdown-menu.vue)

##How to use

1. Include the component
2. Use it
```html
<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>
```
&nbsp;
#<center>🎉✌️🙏</center>
&nbsp;