This article is about my experiences with building a custom navigation using React-navigation/native. Link to navigation docs
I encountered two major problems during this assignment. The first issue was setting a custom button in the middle of bottom navigation, and the second was implementing the animation under the icons. In the case of the latter, we need to use a Reanimated library. Link to Reanimated docs
The whole app is prepared in Typescript which I highly encourage you to use. That said, you will find the most challenging typing at the end of this article, so those who use bare javascript can follow along easily.
As you can see we have four icons and one big button between them. This button is responsible for opening a modal. It will look like iOS modals. Under the icons there is a dot that indicates the currently opened screen. Dot movement is animated with the React Reanimated library, which my colleagues have described in other posts available on our blog.
My biggest challenge during this task was placing the circle button at the proper height. In react-native on the Android system, if part of the button is above the container, it’s not clickable. This feature does not allow me to use absolute positioning. That said, we can now start building it from scratch.
If you want to install the whole app with the necessary packages manually, you are free to do so, but it’s not necessary to follow this article. I’ve created a starter app where I’ve listed all the required dependencies. Link to starter app
Complete code from this article is available here. Complete App
If you decide to install everything by yourself, be sure to configure Reanimated properly.here
I will use @shopify/restyle to make styling easier but feel free to go with anything you want.
Create bottom tab navigation
First of all, we have to make some dummy screens. These screens will contain only Welcome + screen name text to let us know if navigations are working properly. Next, we have to create our BottomTabNavigator file and put our screens there.
import React, {FC} from 'react';
import {Box, Text} from '../../utils/theme';
import {SafeAreaWrapper} from '../../components/SafeAreaWrapper';
export const Dashboard: FC = () => (
<SafeAreaWrapper isDefaultBgColor>
<Box margin="xl">
<Text variant="title1">Welcome in dashboard</Text>
</Box>
</SafeAreaWrapper>
);
// Dashboard.tsx
As you can see, we don’t use regular Bottom tab navigation, but we provide a custom component.
The reason is that we need to do some tricks regarding our dot animation and circle button placement. Let’s create our TabsUi component. We have to pull the container 5px down. Without it, we will have a break between the safe area and our tabs container.
Our TabUi component receives **tabs** props, thanks to which we can calculate the width of a single bottom nav button.
```jsx
const tabWidth = useMemo(() => windowWidth / tabs.length, [tabs.length])
We need it inside our next two components, i.e. TabsHandler and AnimationDot. Pass the props destructured with spread operator (…) and build these components.
Here, an important part is ‘state’ props, which is accessible by every component wrapped by NavigationContainer, and gives us detailed info about our navigation state. The one we need in this case is an index of the active tab. That’s why we pass the activeTabIndex={state.index} further.
Important
If you are using @react-navigation/native in version 6.x or higher, the “state” props is no longer available. Instead, you can use this workaround by using useRoute and useNavigationState.
In TabsHandler.tsx we will map through our tabs variable, depending on the name we will display an icon or a circle button, and pass different onPress functions to them, but let’s start from the beginning.
First we will create a new component called TabsHandler, where we will map through the tabs array. Inside our map function, we define the onPress function. If the tab name is RequestModal, we will navigate to the RequestVacation component, otherwise, we will open the standard screen. Request vacation is a screen that looks like iOS modal. Next, we need to do some styling magic to place our button in the middle of the bottom bar.
The above code contains the rest of our components. Again, we check if our tab.name is equal to RequestModal, if this returns true, we return our button which is just a normal react component. If it returns false, we return our icon inside a TouchableOpacity component. Icons are rendered through a function called getBottomTabIcon. This function accepts four props: the name of the tab that we are rendering, the currently active tab, the color value for an inactive icon, and the color value for an active icon.
The next step is to add our icons to the bottom tab bar. We will use the already installed packages: React-native-svg and React-native-svg-transformer.
Even though there’s nothing really special in our Circle Button, let’s try to create it by mixing two svg icons. One of them is the circle button, and another one is its background which is placed inside RectButton. It’s a special type of button which gives us the ripple effect on the Android system. We need to move the background a bit lower because this icon is too high, that’s why we are setting a negative bottom value on it.
Next, we place our icons in ./assets and import them in the getBottomTabIcon function. Here we use the switch function and as an expression, we pass our tab name. Based on our activeTab name, we pass the inactive or active color value to our icons.
At this point your app should look like this:
I have already mentioned the AnimationDot component, which we could omit in terms of application usability, but it features a fancy animation and makes your app livelier. It’s also a good occasion for a React Reanimated library training.
Add animated dot
The animation dot is another regular component which accepts two props. The first one is the width of a single tab, which will let us position our dot in the center of each tab. The second is the active tab index, which we will need to let our dot know which tab it should stay under.
First, we need to import a few things from the Reanimated library.
A full description of each is available in the official docs. here. That said, I will shortly describe those used in this component.
Animated - allows us to create an Animated component like Animated.View. Without it, our Animation wouldn’t run at all.
Interpolate - a method for interpolating value passed to it. We are passing the value which we want to interpolate, input range as an array, and output range as an array. This method allows for more precise control of our animation.
useAnimatedStyle - a hook for creating an association between shared values and view styles. It returns a style object, which we are passing to our Animated component.
useSharedValue - a hook for holding our values which we want to animate.
withSequence - a function allowing us to run animation in a sequence, for example, lower element height from 5px to 0 and back to 5.
withTiming - a function for controlling the duration of our animation.
All this theory may sound complicated, but as soon as we implement this in our project, everything will be clear.
Most animations start from declaring shared values. We will need three of those.
A starting point is the place where the dot is in the first render. Width and Height are just our dot sizes
Our NavigationDot component returns our dot inside a container, which also requires some styling to be displayed properly. Here we use Animated.View, which was already mentioned above. As for Dot, we pass to it the value returned by useAnimatedStyle which is called progressStyle in here.
The dot moves from tab to tab, and during that movement, its height and width values decrease and increase. This tells us to compute these three values inside the useAnimatedStyle hook, and that’s exactly what we are going to do.
Our progressStyle const returns three values: transform:{translateX}, width, and height. Animation setup is finished, now we need to run it.
Here with help comes useEffect Hook, which will re-render with every activeTabIndex change. Inside we declare what our animation should look like, and how long it should take.
TranslateX indicates the dot position, so we move it to startingPos (e.g 32px) + activeTabIndex(e.g 1) * width of single tab. This animation is made in 600ms as the duration value state. Next, we animate the dot width and height.
Width is increased to single tab width, and then back to 5px. Height is decreased to 0, and then increased to 5px.
By using withSequence, we divide those animations inside into two parts. Each part takes 300ms so that they end at the same time as the translateX animation.
I’m happy with the results. That said, if you have any ideas for improving them, then go ahead. Don’t forget to share it with us on our Twitter. here
So far, we have prepared our bottom tab and icons. Now we will add a Drawer navigation, and connect everything.
Create drawer tab
This part of the article will be the shortest. I leave styling, animations, etc. to your imagination. It’s just about clipping it together with other navigations (Stack and Bottom).
Add the next component named DrawerNavigator in our navigation folder. It contains three screens: the first one is our Home Screen where we have our BottomTabNavigator created earlier. The next two are screens without the bottom bar and with a disabled swipe, which means we are not able to open a drawer within them.
In order to be consistent with one name (Home), we import BottomTabNavigator like this:
import { BottomTabNavigator as Home } from ‘./BottomTabNavigator’
At the bottom of the drawer, we add a logout button. For this feature, we need to render a custom drawer content, just as we did before with the bottom bar.
import React from 'react';
import {createDrawerNavigator} from '@react-navigation/drawer';
import {About} from '../screens/about/About';
import {Settings} from '../screens/settings/Settings';
import {DrawerRoutes} from './types';
import {CustomDrawerContent} from './CustomDrawerContent';
import {BottomTabNavigator as Home} from './BottomTabNavigator';
const Drawer = createDrawerNavigator<DrawerRoutes>();
export const DrawerNavigator = () => {
return (
<Drawer.Navigator
initialRouteName="Home"
drawerContent={props => <CustomDrawerContent {...props} />}>
<Drawer.Screen name="Home" component={Home} options={{title: 'Home'}} />
<Drawer.Screen
name="About"
component={About}
options={{title: 'About', swipeEnabled: false}}
/>
<Drawer.Screen
name="Settings"
component={Settings}
options={{title: 'Settings', swipeEnabled: false}}
/>
</Drawer.Navigator>
);
};
// DrawerNavigator.tsx
Our custom content will be the CustomDrawerContent component, which renders all screens passed in props and our button component. A bit of flexbox styling and we have our drawer ready.
Depending on how your authorization is configured, you should pass a proper function to the logout button, but that’s not covered in this article.
Dummy stack navigation
In our application, stack navigation is an onboarding flow. It means that if the user is not logged in, they have access only to this part of the application. I’m not going to create all these screens here, this is just an example stack navigation that we are using.
Now when every single navigation is ready to use, it’s time to combine them.
Connect all navigations
First, let’s create the index.tsx file. In a real app, it will look like a commented code. If user.firstName is present, we return ModalNavigation, which contains DrawerNavigation, BottomNavigation, and our modal screen. If not, we will show the user AuthStackNavigation - onboarding flow. For the sake of simplicity, I’ve decided to render only ModalNavigation.
This part is a bit tricky because the whole component is a Navigator, with mode set to “modal”, which gets TransitionPresets; this makes it look like an iOS modal. Our default route is DrawerNavigator, which just renders our app, but when we click on the circle button it opens a modal.
Add typescript to our navigation
You can omit this part if you’re not using typescript yet (I think that you should start). Otherwise, create a types.ts file in the navigation folder.
First, we need types for each navigator created earlier. Before that we have a helper function for nested navigations.
type NestedNavigatorParams<ParamList> = {
[K in keyof ParamList]?: { screen: K; params?: ParamList[K] }
}[keyof ParamList]
// navigation/types.ts
This type takes a ParamList argument which in this case will be a list of our screens in a single stack. K is a single screen here. This tells us that K must receive a screen and can receive params. When we define our main type, we wrap each stack in that type.
This type says that our AuthStack can only have screens listed here. Undefined means that the screen is not accepting any props. The same pattern applies to other stacks. The only difference is when route navigates to another Stack or gets props, then we give this stack types instead of undefined.
Now we need to perform some more complicated operations to be able to use the useNavigation hook and navigation prop. First of all, we need some imports from @react-navigation.
import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'
// navigation/types.ts
Those are predefined types that we will combine with the types that we wrote before. Again, every stack has to be typed, but I will only describe the most complicated ones. You can browse the rest of them in project files. types.ts
This is the type needed for useNavigation hook inside Drawer Navigation screens. It takes a RouteName argument, which must be one of the DrawerRoutes keys.CompositeNavigatorProp is used for nested navigators. Because its definition is quite complicated, I will refer you to the official documentation. CompositeNavigatorProps - ReactNative docs.
This is how it will be used in the code (in Settings screen which is in drawer navigator):
The idea of this type is very similar to the one above, but here we are typing navigation and route separately. In route typing we are using imported RouteProp with two arguments, first the desired stack types and second the name of the route that we will pass as an argument.
Summary
I hope that you found this article useful. I am well aware that combining all navigation types can be confusing, but hopefully I’ve cleared up a few things for you. I encourage you to at least try to write it by yourself but feel free to copy and improve my code :)
Thank you! Your submission has been received! 📨
Oops! Something went wrong while submitting the form.
Share our work
React-navigation/native
React-navigation/bottom-tabs
React Native
Mobile development
Reanimated
See related posts
Custom color picker animation with React Native Reanimated v2
Color picker
React Native
Mobile development
Reanimated
React native reanimated
It'll guide you through the process of creating a color picker animation using React Native Reanimated v2 hooks: useSharedValue, useAnimatedGestureHandler, useAnimatedStyle.
How to set video as a background in React Native application
React Native Video
Video background
React Native
Explore step-by-step guidance on integrating video backgrounds into your React Native applications using the react-native-video library. This article provides practical tips and code examples to enhance your app's UI with dynamic video elements, suitable for both beginners and experienced developers in React Native.