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:
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:
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.
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 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)
})
}
},
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:
Also if you want to see a specific widget for next week, post it in the comments section.
Top comments (0)