Drawer navigation menu using CSS and Vue JS
(Originally posted on https://dev.to/debadeepsen/drawer-navigation-menu-using-css-and-vue-js-hho)
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
- you are familiar with Vue JS and Vue CLI
- 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.
<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.
<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.
.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
andvh
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.
<script>
export default {
data() {
return {
drawerVisible: false,
};
},
};
</script>
We'll modify the CSS for the drawer by adding a transition:
.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
.
<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:
<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.
<!-- 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 -
<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.
<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 thebody
tag tohidden
.
That's it! Here's the full code running on CodeSandbox. Feel free to ask me questions in the comments!