Article

Building a mobile navigation drawer with Vue.js

Remco Hendriksen on Nov 6, 2020
component javascript scss events props
Last updated: Dec 20, 2021

A navigation drawer is the most common way to navigate through a website on mobile devices. In this post i will show you how to do it.

<template>
  <div class="drawer" :class="{ 'drawer--show': show }">
    <div class="drawer__bg" @click="$emit('close')"></div>
    <div class="drawer__content">
      <router-link
        v-for="link in links"
        :key="link.path"
        class="drawer__item"
        :to="link.path"
      >
        {{ link.title }}
      </router-link>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      links: [
        {
          path: "/home",
          title: "Home",
        },
        {
          path: "/articles",
          title: "Articles",
        },
        {
          path: "/about",
          title: "About",
        },
      ],
    }
  },
}
</script>
<style lang="scss">
.drawer {
  pointer-events: none;

  &__bg {
    background-color: rgba($black, 0.75);
    width: 100%;
    height: 100%;
    position: fixed;
    left: 0;
    top: 0;
    opacity: 0;
    transition: opacity 1s ease;
    z-index: 10;
  }
  &__content {
    background-color: $white;
    display: flex;
    flex-direction: column;
    position: fixed;
    right: 0;
    top: 0;
    bottom: 0;
    width: 100%;
    max-width: $spacing * 50;
    z-index: 11;
    transform: translateX(100%);
    transition: transform 1s ease;
  }
  &__item {
    color: $black;
    padding: $spacing * 4;
    text-align: center;
  }

  &--show {
    pointer-events: all;

    .drawer__bg {
      opacity: 1;
    }
    .drawer__content {
      transform: translateX(0);
    }
  }
}
</style>

If you don't understand why this structure is used, please read the post How to structure your Vue.js application the right way.

How is it suppose to work

  • We assume that we have a button in a toolbar which has to be clicked to open the drawer
  • A click on that button toggles the show-prop on the drawer
  • The drawer can be closed by clicking on the background of the drawer

Styling the drawer

The SCSS does the most of the work in this component. First we style the drawer as if it were always open. We make two elements with position:fixed; applied so that they always float over the content. Setting a high enough z-index is important.

.drawer {
  pointer-events: none;

  &__bg {
    background-color: rgba($black, 0.75);
    width: 100%;
    height: 100%;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 10;
  }
  &__content {
    background-color: $white;
    display: flex;
    flex-direction: column;
    position: fixed;
    right: 0;
    top: 0;
    bottom: 0;
    width: 90vw;
    max-width: $spacing * 50;
    z-index: 11;
  }
}

If we are happy with the result we add transform: translateX(100%)to the drawer__content to hide to right of the screen. The background is hidden by setting opacity: 0.

.drawer {
    pointer-events: none;
  &__bg {
    ...
    opacity: 0;
    transition: opacity 1s ease;
    ...
  }
  &__content {
    ...
    transform: translateX(100%);
    transition: transform 1s ease;
  }

When the drawer--show class is applied we transition the elements back to opacity: 1 and transform: translateX(0). Also it's important to enable the pointer-events again. Otherwise nothing on page is clickable again. Try it ;).

.drawer {
  &--show {
    pointer-events: all;

    .drawer__bg {
      opacity: 1;
    }
    .drawer__content {
      transform: translateX(0);
    }
  }
}

Markup and javascript

The heavy lifting is done now. We only have to put the html in place and add some Vue-data and were done. Let's go on.

<template>
  <div class="drawer" :class="{ 'drawer--show': show }">
    <div class="drawer__bg" @click="$emit('close')"></div>
    <div class="drawer__content">
      <router-link
        v-for="link in links"
        :key="link.path"
        class="drawer__item"
        :to="link.path"
      >
        {{ link.title }}
      </router-link>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    show: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      links: [
        {
          path: "/home",
          title: "Home",
        },
        ...
      ],
    }
  },
}
</script>

The two important things here are setting the dialog--show class and emitting the close event.

{ 'drawer--show': show } sets the class drawer--show when the show-prop is set to true.

@click="$emit('close')" emits an event out of the component to let other components know that the drawer is closed.

Usage

A small example of how to use this drawer from within een layout.

<template>
  <div>
    <drawer :show="showDrawer" @close="showDrawer = false" />
    <main>
      <Nuxt />
    </main>
  </div>
</template>
<script>
import Drawer from "~/components/Drawer"
export default {
  components: {
    Drawer,
  },
  data() {
    return {
      showDrawer: true,
    }
  },
}
</script>
Leave a comment below if you have got any questions about this article.

Leave a comment

More articles