Creating a Responsive Navbar using Bulma and Vue.js

A simple guide on how to create a responsive navbar on a Vue.js project using Bulma CSS framework.

Published on May 21, 2020

First things first, I’ll be using Nuxt.js as my framework but you can follow along using anything else, modifications might be needed though.

Setup

Since we are going to use Bulma on our project let’s add it as a dependency, in my case I’ll be installing @nuxtjs/bulma, if you are using plain Vue you can install the bulma package instead.

yarn add @nuxtjs/bulma

Additional packages may be required depening on your project, such as node-sass and sass-loader if you are using sass.

Next, instead of registering the package into the modules section, we will be doing the exact opposite, if you aready have it there just comment it out. You don’t have to do this if you want to style everything within the single file components themselves, A.K.A. <style scoped> tag.

// nuxt.config.js

export default {
  modules: [
    // '@nuxtjs/bulma' remove if using a separated file
  ]  
}

Now, create a new folder to host our stylesheets, I will be using assets/stylesheets.

In that folder, create a new file and add the following contents:

// assets/stylesheets/main.scss

// Bulma variables
@import "~bulma/sass/utilities/all";
// Bulma framework
@import "~bulma";

And last but not least, add the newly created file to the global css path:

// nuxt.config.js

export default {
  css: ['~assets/stylesheets/main.scss']
}

Great, now that our CSS framework is imported and ready to be used we’re going to create the navbar component.

Creating The Navbar

Create a new component under the components folder and name it whatever you want, I’ll call mine Navbar.vue.

Based on Bulma’s documentation a simple navbar would look like the code below:

<!-- components/Navbar.vue -->

<template>
  <header class="navbar is-fixed-top is-transparent is-spaced">
    <div class="container">
      <div class="navbar-brand">
        <nuxt-link to="/" class="navbar-item">
          <h1 class="is-size-4">Marlos Pomin</h1>
        </nuxt-link>
        <a
          role="button"
          class="navbar-burger"
          aria-label="menu"
          data-target="collapse"
        >
          <span aria-hidden="true" />
          <span aria-hidden="true" />
          <span aria-hidden="true" />
        </a>
      </div>
      <div id="collapse" class="navbar-menu is-paddingless">
        <nav class="navbar-end">
          <nuxt-link to="/" class="navbar-item">Home</nuxt-link>
          <nuxt-link to="/blog" class="navbar-item">Blog</nuxt-link>
        </nav>
      </div>
    </div>
  </header>
</template>

Here’s the demo so you can see the result:

The problem right now is that it does’t do anything, it’s responsive but not interactive in any way.

In the next step will be adding some functionality to it using a some Vue magic.

Interactivity

Create some control vars under the <script> tag:

<!-- components/Navbar.vue -->

<script>
export default {
  data() {
    return {
      isActive: false,
      showNavbar: true
    }
  }
}
</script>

Now, add a click handler to toggle isActive:

<!-- components/Navbar.vue -->

<template>
  <a
    :aria-expanded="isActive"
    :class="{ 'is-active': isActive }"
    @click="isActive = !isActive"
  >
</template>

The final result will probably look like the code block below:

<template>
  <header class="navbar is-fixed-top is-transparent is-spaced">
    <div class="container">
      <div class="navbar-brand is-family-secondary">
        <nuxt-link to="/" class="navbar-item">
          <h1 class="is-size-4">Marlos Pomin</h1>
        </nuxt-link>
        <!-- @click handler goes here, make sure to scroll a bit to see it. -->
        <a
          :aria-expanded="isActive"
          :class="{ 'is-active': isActive }"
          role="button"
          class="navbar-burger"
          aria-label="menu"
          data-target="collapse"
          @click="isActive = !isActive"
        >
          <span aria-hidden="true" />
          <span aria-hidden="true" />
          <span aria-hidden="true" />
        </a>
      </div>
      <div
        id="collapse"
        :class="{ 'is-active': isActive }"
        class="navbar-menu is-paddingless"
      >
        <nav class="navbar-end">
          <nuxt-link to="/" class="navbar-item">Home</nuxt-link>
          <nuxt-link to="/blog" class="navbar-item">Blog</nuxt-link>
        </nav>
      </div>
    </div>
  </header>
</template>

<script>
export default {
  data() {
    return {
      // control variables
      isActive: false,
      showNavbar: true
    }
  }
}
</script>

And the demo would be:

Now clicking the menu works, neat right?! As a bonus I’ll show you how you can hide your navbar upon scroll and a way to close the menu if the user resizes the window.

Add the following snippet to your navbar style:

.navbar {
  transition: transform 200ms ease-out;

  &.is-transformed {
    transform: translate3d(0, -100%, 0);
  }
}

This will take care of the animation when the navbar is toggled by a scroll or resize event which is controlled by hideNav().

<template>
  <header
    :class="{ 'is-transformed': !showNavbar }"
    class="navbar is-fixed-top is-transparent is-spaced"
  >
    <div class="container">
      <div class="navbar-brand is-family-secondary">
        <nuxt-link to="/" class="navbar-item">
          <h1 class="is-size-4">Marlos Pomin</h1>
        </nuxt-link>
        <a
          :aria-expanded="isActive"
          :class="{ 'is-active': isActive }"
          role="button"
          class="navbar-burger"
          aria-label="menu"
          data-target="collapse"
          @click="isActive = !isActive"
        >
          <span aria-hidden="true" />
          <span aria-hidden="true" />
          <span aria-hidden="true" />
        </a>
      </div>
      <div
        id="collapse"
        :class="{ 'is-active': isActive }"
        class="navbar-menu is-paddingless"
      >
        <nav class="navbar-end">
          <nuxt-link to="/" class="navbar-item">Home</nuxt-link>
          <nuxt-link to="/blog" class="navbar-item">Blog</nuxt-link>
        </nav>
      </div>
    </div>
  </header>
</template>

<script>
import throttle from 'lodash/throttle'

export default {
  data() {
    return {
      isActive: false,
      showNavbar: true,
      lastScrollPosition: 0
    }
  },
  mounted() {
    this.$nextTick(() => {
      window.addEventListener('resize', throttle(this.closeMenu, 500))
      window.addEventListener('scroll', throttle(this.hideNav, 250))
    })
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.closeMenu)
    window.removeEventListener('scroll', this.hideNav)
  },
  methods: {
    closeMenu() {
      this.isActive = false
    },
    hideNav() {
      const currentScrollPosition =
        window.pageYOffset || document.documentElement.scrollTop
      if (currentScrollPosition < 0) return
      if (Math.abs(currentScrollPosition - this.lastScrollPosition) < 60) return
      this.showNavbar = currentScrollPosition < this.lastScrollPosition
      this.lastScrollPosition = currentScrollPosition
      setTimeout(this.closeMenu, 250)
    }
  }
}
</script>

As for the result, you are looking at it! scroll up and down to see the effect.

Thanks for reading!

how-todevbulma

Marlos Pomin
Full Stack Developer & Retoucher based in Brazil, also a casual pentester.