DEV Community

Cover image for Making an animated nav component - WotW
Eder Díaz
Eder Díaz

Posted on • Edited on

Making an animated nav component - WotW

Welcome to the Widget of the Week series, where I take gifs or videos of awesome UI/UX components, and bring them to life with code.

Today's the turn for a navigation component with four colorful icon buttons.The inspiration comes from this submission and it looks like this:

reference

Preparations

For today's widget we will be using Vue.js for the interactions, and TweenMax for animations. If you want to follow along you can also fork this codepen template that already has the dependencies.

We will also use FontAwesome icons, so make sure that you add this link to import them:

<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous">
Enter fullscreen mode Exit fullscreen mode

The initial markup

We will start with the HTML. For this component we need just a container and the buttons. As I just mentioned above, we will use the FontAwesome icons for the buttons, they're not exactly the same as in the original submission but they're good enough.

<div id="app">
  <div class="btn-container">
    <div class="btn">
      <i class="fas fa-comment"></i>
    </div>
    <div class="btn">
      <i class="fas fa-user"></i>
    </div>
    <div class="btn">
      <i class="fas fa-map-marker"></i>
    </div>
    <div class="btn">
      <i class="fas fa-cog"></i>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Right now we should have the four icons, it's time to make it look more like the final product.

Styling

In the container we need a background color, I'll use black for now but later we will change that programatically. Also I'll use flex and justify-content to center the elements horizontally, then just some padding to vertically align them.

.btn-container {
  display: flex;
  background-color: black;

  /* center vertically */
  padding-top: 150px;
  padding-bottom: 150px;
  /* center horizontally */
  justify-content: center;
}
Enter fullscreen mode Exit fullscreen mode

For the buttons there's a bit of more work needed, we'll use inline-block so that they render beside each other.

We need to define the sizes of both the button and it's content, along with some default colors, then use border radius to make them circles and also a couple of rules to align the icons correctly:

.btn {
  display: inline-block;
  cursor: pointer;
  width: 50px;
  height: 50px;
  margin: 5px;
  font-size: 25px;
  color: gray;

  /*  Circles  */
  border-radius: 25px;
  background-color: white;

  /* center icons */
  text-align: center;
  line-height: 50px;

  /* remove touch blue highlight on mobile */
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
Enter fullscreen mode Exit fullscreen mode

And now we should have something like this:

html+css

The behavior

Now in our Vue instance we will start declaring the data that we need to use on the component. With a color picker, I took the different colors for buttons and backgrounds and put them inside a structure so we can reference them in the future:

new Vue({
  el: '#app',
  data: {
    buttons: [
      {icon: 'comment', bgColor: '#DE9B00', color: '#EDB205'},
      {icon: 'user', bgColor: '#3EAF6F', color: '#4BD389'},
      {icon: 'map-marker', bgColor: '#BE0031', color: '#E61753'},
      {icon: 'cog', bgColor: '#8E00AC', color: '#B32DD2'}
    ],
    selectedBgColor: '#DE9B00',
    selectedId: 0
  },
})
Enter fullscreen mode Exit fullscreen mode

Also I already declared a variable that will have the current background color and the id of the selected button.

Since we also have the icon data inside the buttons array, we can change our HTML code to render with a v-for the buttons and become more dynamic:

<div id="app">
  <div class="btn-container" :style="{'backgroundColor': selectedBgColor}">
    <div v-for="(button, index) in buttons" 
         :key="index" 
         @click="selectButton(index)"
         :ref="`button_${index}`"
         class="btn">
      <i :class="['fas', `fa-${button.icon}`]"></i>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

This code is also already binding the background color to the btn-container div style.

getting closer

Notice that we added an @click handler that should trigger a function called selectButton, also the ref attribute will help us reference the buttons when we need to animate them.

Clicking a button

We need to declare first the selectButton method in our Vue instance:

// ... data,
  methods: {
    selectButton (id) {
      this.selectedId = id
    }
  }
Enter fullscreen mode Exit fullscreen mode

After this the selectedId will change on every click to values between 0-3, but that doesn't seem to do anything to our component. We need to start animating things!

Let's begin animating the simplest part, the background color. For that we need to make a computed property that will get the selected button data which will help us to get the corresponding background color.
Later when we change the selectedId we will be able to tween the color to the current selected one.

// ... data
 methods: {
    selectButton (id) {
      this.selectedId = id
      this.animateBgColor()
    },
    animateBgColor () {
      TweenMax.to(this, 0.2, {
        selectedBgColor: this.selectedButton.bgColor
      })
    }
  },
  computed: {
    selectedButton () {
      return this.buttons[this.selectedId]
    }
  }
Enter fullscreen mode Exit fullscreen mode

We should have a working transition of the background color when clicking any button.

Animating the buttons

Buttons are going to be a bit trickier to animate. For starters, we will need to save a reference to the previously active button and the next button to activate.

To achieve that we can use $refs with the index of the selected button before setting the new one, like this:

// ... data
  methods: {
    selectButton (id) {
      const previousButton = this.$refs[`button_${this.selectedId}`]
      const nextButton = this.$refs[`button_${id}`]
      // ... rest of code
Enter fullscreen mode Exit fullscreen mode

Now that we have those references we should be able to run a couple of methods, one to deactivate the previous button and the other one to activate the new one:

// ... methods
    selectButton (id) {
      const previousButton = this.$refs[`button_${this.selectedId}`]
      const nextButton = this.$refs[`button_${id}`]

      this.selectedId = id
      this.animateBgColor()

      this.animateOut(previousButton)
      this.animateIn(nextButton)
    },
    animateIn (btn) {      
      // TODO activate button
    },
    animateOut (btn) {
      // TODO deactivate button
    }
Enter fullscreen mode Exit fullscreen mode

Before coding that part we need to stop and think how the buttons should animate. If we analize the gif, the button animation can be split in two changes, one for the colors of the button and icon and the other one for the width of the button.

The colors transition looks really straightforward, the button's background changes to white when inactive, and to the color property when active. For the icon, it just changes between gray and white.

The interesting thing is with the button width animation, it looks kinda "elastic" because it goes a bit back and forth at the end.

Playing with the GSAP ease visualizer I came with the props that closely match the easing of the original animation. Now we can finish coding the animateIn and animateOut methods:

// ... methods
   animateIn (btn) {      
      // animate icon & bg color
      TweenMax.to(btn, 0.3, {
        backgroundColor: this.selectedButton.color,
        color: 'white'
      })

      // animate button width
      TweenMax.to(btn, 0.7, {
        width: 100,
        ease: Elastic.easeOut.config(1, 0.5)
      })
    },
    animateOut (btn) {
      // animate icon color
      TweenMax.to(btn, 0.3, {
        backgroundColor: 'white',
        color: 'gray'
      })

      // animate button width
      TweenMax.to(btn, 0.7, {
        width: 50,
        ease: Elastic.easeOut.config(1, 0.5)
      })
    }
  },
Enter fullscreen mode Exit fullscreen mode

We're almost done, there's just a small detail. When the app starts, the component doesn't look to have a selected button. Luckily that can be quickly solved by calling the selectButton method inside the mounted hook:

  mounted () {
    // initialize widget
    this.selectButton(0)
  }
Enter fullscreen mode Exit fullscreen mode

And now the final result!

That’s it for this Widget of the Week.

If you're hungry for more you can check other WotW:

Also if you want to see a specific widget for next week, post it in the comments section.

Was the article useful? You can support my Coffee Driven Posts here:

Top comments (0)