Making an animated tab bar with React Native
Micro-interactions are a key element to make your app stand out and to make users want to interact with it. Tab bars are good candidates for micro-interactions as they are one of the most used components of an app, which ensures that the users will play with it, giving them an immediate sense of quality.
In this post, we will see how to create the “Flashy tab-bar” designed by Cuberto. Cuberto already implemented it for iOS and for Android. Making it with React Native is an interesting exercise as it will lead us to play with hooks, transforms and clipping views creatively.
The skeleton of our tab bar
To achieve our goal, we will need to create two components, the
TabBar itself and the
TabItem made of an icon, a label and a dot-indicator. When the item is inactive, only the icon is displayed; when it is active, the icon is hidden and the label and dot-indicators are visible.
First, let’s start with the non-animated
And here is how they look:
Now let’s create the
TabBar component. It displays all the items in a row and stores the index of the active item in its state, updating it each time one is pressed:
And here is how it looks in action:
From basic to classic 🤓
Now that we have created our non-animated tab bar, it is time to decompose the animation we want to achieve. Here is what happens when an item goes from inactive to active:
- The dot indicator appears by scaling up from 0% to its normal size
- The label appears with some kind of diagonal transition
- The icon disappears in a similar fashion
- The icon and label are translated vertically at different speeds
The easiest parts to implement are the dot indicator scaling and the icon/label translations, so let’s starts with these. For this, we will use React Native Animated and a single Animated value to drive all of our animations. This value will go from 0 to 1 when the item goes from inactive to active. For a more natural animation, we will use spring-based animations.
First, let’s introduce the
useSpring() hook that will make it easier to start/stop our animation when the
active prop changes:
Here, we use
useEffect() to start/stop the
Animated.spring animation when the
to value changes. The API of this hook is inspired by react-spring (which unfortunately does not support running animations on the native thread at the moment).
Let’s use this new hook to animate the scale and translations of
TabItem’s subviews. We will compute scale/translations values by interpolating them from the animated value returned by
useSpring(). For now, we will replace icon and label’s transitions by a simple fade-in/fade-out.
Note also that we also removed the
active && conditions that were used to show/hide subviews. We did this to be able to animate the disappearing views as you cannot animate unmounted components.
Let’s see how the first iteration of our animated tab bar looks like:
That’s definitely better, but it looks quite classic. As a user, I don’t want to play with it as much as I want to play with Cuberto’s tab bar. Let’s see if we can change this.
From classic to epic 🤩
What makes the Cuberto’s tab bar really stand out is the way views appear/disappear. They don’t fade-in/fade-out but they appear by revealing themselves with a diagonal-shaped transition. There are several ways to achieve this effect with React Native:
- We could mask the views with an animated mask using react-native-masked-view but unfortunately, this library does not support animated masks on Android yet.
- We could clip the views with a slightly rotated parent with
overflow: "hidden"and use vertical translations to reveal/hide them but this could get tricky as we would need to apply the inverse transform on the child views to keep them from moving.
- We could also cover the views with an opaque white view that is translated vertically from bottom to top and slightly skewed to give it a diagonal shape. This works well, the only downside with this technique is that it cannot be used on a translucent tab bar (which is not the case here).
In this post, we will use the latter solution as it is the easiest to implement and it works well across all platforms.
In order to do that, let’s introduce a new
DiagonalTransition component that makes its content appear/disappear by covering it with diagonal-shaped cover view. The position of cover view is controlled by the
visibility prop that goes from 0 to 1.
To have a better understanding of what this component does, here is a look at the component in 3 different configurations:
- At left, with
overflow: "visible"and a
- In middle, with
overflow: "hidden"and a
- At right, with
overflow: "hidden"and a
Putting it all together
Now that we have our new
DiagonalTransition component, let’s replace the opacity transition with it. It is just a matter of wrapping the icon and label of
TabItem with a
And let’s run it:
And voilà! The tab bar we made is not that far off from Cuberto’s initial design. We could tweak the animation a bit but the result already looks pretty convincing to me. It also runs 60fps even on low-end Android devices thanks to the use of Animated
The same technique can be applied to implement all kinds of transitions, using different shapes or different transforms (you can replace the translation of the cover view by a 90° rotation for example).
I hope you enjoyed this article! Please share your comments with us in the section below👇