Drawer navigation menu using CSS and Vue JS
Tags: javascript, css, vue
Published: 2020-11-03 12:03:01

One of the things that I have found impressive in mobile apps is the drawer that opens from the side (left or right) which typically contains navigation links. The same behavior is replicated in many websites, not just for menu, but in some cases to display other things like a list of search filters. Recently, I had to implement this for a Vue JS project I was working on. There are many `npm` packages for it, but I eventually decided to make it myself, because I could design it exactly the way I liked it, and it would also give me a chance to learn about the inner workings of such prebuilt packages. Turns out, it's pretty simple - here's how I did it.

## Assumptions
For the purpose of this article, I will assume that
1. you are familiar with Vue JS and Vue CLI
2. you have a basic understanding of CSS

## Project setup
I created a Vue project using Vue CLI, and went to the .vue file where I wanted the menu to be. I also added some content and basic css to make it look reasonably pretty.

```vue
<template>
  <div>
    <div style="text-align:right">
      <button class="toggle"><i class="las la-bars"></i> Show Menu</button>
    </div>
    <h1>Welcome to Vue JS</h1>
    <h3>This is a sample page with some sample content</h3>
    <p>
      Alone during the day, in my room or out of doors, I thought аbout the
      waiter more than about my раrеnts; as I now realize, it was а kind of
      love. I had nо desire for contact, I wanted only to bе near him, and I
      missed him on his day off. When he finally reappeared, his
      black-and­-white attire brought lifе into the rооm and I acquired а sense
      of color. Не always kept his distance, even when off duty, and that may
      have accounted for my affection. Оnе day I ran into him in his street
      clothes at the bus-station buffet, now in the role of а guest, and there
      was no difference between the waiter at the hotel and the young man in the
      gray suit with а raincoat over his аrm, resting оnе foot on the railing
      and slowly munching а sausage while watching the departing buses. And
      perhaps this aloofness in combination with his attentiveness аnd poise
      were the components of the beauty that so moved me. Even today, in а
      predicament, I think about that waiter’s poise; it doesn’t usually help
      much, but it brings back his image, and for the moment at least I regain
      my composure.
    </p>
    <p>
      Тoward midnight, оn my last day in the Black Earth Hotel – all the guests
      and the cook, too, had left – I passed the open kitchen on my way to my
      room аnd saw the waiter sitting bу а tub full of dishes, using а
      tablecloth to dry them. Later, when I looked out of my window, he was
      standing in his shirtsleeves on the bridge across the torrent, holding а
      pile of dishes under his right аrm. With his left hand, he took one after
      another and with а smooth graceful movement sent them sailing into the
      water like so many Frisbees.
    </p>
    <p>
      From
      <a
        target="_blank"
        href="https://www.nobelprize.org/prizes/literature/2019/handke/prose/"
        >https://www.nobelprize.org/prizes/literature/2019/handke/prose/</a
      >
    </p>
  </div>
</template>
```
Next, we'll add the `div` that'll contain the menu, and the mask that appears over the page content when the menu is open. I'm keeping it fairly simple.

```vue
    <div class="right-drawer">
      <h1>Menu</h1>
      <h4>Home</h4>
      <h4>About</h4>
      <h4>Stories</h4>
      <h4>Testimonials</h4>
      <h4>Contact</h4>
    </div>
    <div class="drawer-mask"></div>
```
Now, the CSS for it. We'll position both of these absolutely. Intially, the drawer div will have its width set to zero. When we click a button to open our menu, we'll increase its width gradually through a CSS transition, and do the same with the opacity of the mask. When the menu is closed, we'll do the opposite. We'll also run the padding through the same transitions, to make sure no part of the drawer is "peeking out" in its closed state.
```css
.right-drawer {
  position: absolute;
  top: 0;
  right: 0;
  width: 0; /* initially */
  overflow: hidden;
  height: 100vh;
  padding-left: 0; /* initially */
  border-left: 1px solid whitesmoke;
  background: white;
  z-index: 200;
}

.drawer-mask {
  position: absolute;
  left: 0;
  top: 0;
  width: 0; /* initially */
  height: 100vh;
  background: #000;
  opacity: 0.3;
  z-index: 199;
}
```
> `vw` and `vh` stand for view width and view height respectively. They're handy units that you can use to quickly set the dimensions of elements relative to the full screen width or height.

## Showing the drawer
Now for the interactions. Once again, extremely simple. We'll add a state variable called `drawerVisible`, which will control the opened and closed state of the drawer.
```vue
<script>
export default {
  data() {
    return {
      drawerVisible: false,
    };
  },
};
</script>
```
We'll modify the CSS for the drawer by adding a transition:
```css
.right-drawer {
  position: absolute;
  top: 0;
  right: 0;
  width: 0; /* initially */
  overflow: hidden;
  height: 100vh;
  padding-left: 0; /* initially */
  border-left: 1px solid whitesmoke;
  background: white;
  z-index: 200;
  transition: all 0.2s; /* for the animation */
}
```
And we'll add a style to the drawer div, to make it behave in accordance with the value of the state variable `drawerVisible`.
```vue
<div
  class="right-drawer"
  :style="{
     width: drawerVisible? '20vw' : '0',
     paddingLeft: drawerVisible? '10px' : '0',
  }"
>
   ...
```
> Remember that when you use dynamic styles in Vue, you are required to bind it to a JavaScript object, unlike how you would have kept its value as a string if it was static.

Lastly, let's attach an event handler to the click event of the "Show Menu" button:
```vue
<button class="toggle" @click="drawerVisible = true">
  <i class="las la-bars"></i> Show Menu
</button>
```
If you've got this far, the drawer should now be working. However, there is another part remaining - to display a translucent mask over the main content while the drawer is up. For that, we just need to alter the dimension and opacity of the mask, as the value of `drawerVisible` changes.
```vue
<!-- We will make the mask fill the screen
    while the menu is visible. Because its z-index
    is 1 less than that of the menu, the menu will 
    still be displayed on top of it -->
    <div
      class="drawer-mask"
      :style="{
        width: drawerVisible ? '100vw' : '0',
        opacity: drawerVisible ? '0.6' : '0',
      }"
    ></div>
```
## Hiding the drawer
We're almost there. All we need now is a way to close the drawer. We'll implement two of them, in fact - a close button inside the drawer, and also allowing the user to close it by clicking on the mask. For the first, add a button inside the drawer like so - 
```vue
<div style="text-align:right; margin:5px">
  <button class="close" @click="drawerVisible = false">
    <i class="las la-times" style="font-size:24px"></i>
  </button>
</div>
```
And simply add a click event handler to the mask will do the trick for the other one.
```vue
<div
  class="drawer-mask"
  :style="{
    width: drawerVisible ? '100vw' : '0',
    opacity: drawerVisible ? '0.6' : '0',
  }"
  @click="drawerVisible = false"
>
</div>
```
> The scrollbars are a problem, especially if you scroll down, because the mask doesn't cover the lower part. That can be addressed by changing the positioning styles of the drawer and the mask, or by turning the scrollbars off, by setting the `overflow` property of the `body` tag to `hidden`.

That's it! Here's the full code running on CodeSandbox. Feel free to ask me questions in the comments!
{% codesandbox blissful-dhawan-4g2y7 %}