Eder Díaz blog

Blog

tips & tricks

Making an animated nav component - WotW

May 16, 2018

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:

animated gif of 4 buttons that when clicked, the background changes color

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"
/>

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>

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;
}

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);
}

And now we should have something like this:

buttons styled like in the reference

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,
  },
});

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>

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

the background now matches the reference

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
    }
  }

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]
    }
  }

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

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
    }

Before coding that part we need to stop and think how the buttons should animate. If we analyze 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)
      })
    }
  },

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)
  }

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:

❮ Back to list