Custom Tab Bar in React Native using SVG, and D3-Shape

By StartxLabs
Date 24-03-21
Custom Tab Bar in React Native using SVG, and D3-Shape
" Custom Tab Bar in React Native using SVG and D3-Shape"

 

Recently I had the opportunity to add tab navigation to my mobile app. Well, judging by the design of the tab bar I knew a traditional tab bar would not be sufficient. So, after an intensive google search, I found the correct approach to build a custom tab bar that obviously includes the usage of SVG.

 

Let me share a quick demo of the end product of what we are gonna build.

 

 

Before continuing, this tutorial assumes you are familiar with react native and react-navigation.Also, we are gonna be using react-native-svg and d3-shape

 

Let's start by importing createBottomTabNavigator from react-navigation-tabs

 

 

import { createBottomTabNavigator } from "react-navigation-tabs";

 

Now the first step to creating a custom tab bar is to modify the tabBarComponent property in the bottom tab navigator config.

 

tabBarComponent: (props) => {
    return <CustomTabBar {...props} />;
},

 

All this piece of code does is tell react-navigation that we are gonna be using a custom tab bar component. Now Let's actually build the CustomTabBar.

 

function CustomTabBar(props) {
    const tabLength = 5;
    const tabWidth = useMemo(() => (wWidth - 100) / (tabLength - 1), [tabLength]);
    const { onTabPress, navigation } = props;
    const { routes, index } = props.navigation.state;

    return (
        <View>
            <TabsShape {...{ tabWidth, index }} />
            <View {...StyleSheet.absoluteFill}>
                <TabsHandler {...{ routes, tabWidth, onTabPress, index, navigation }} />
            </View>
        </View>
    );
}

 

Let's just break it down what we have done so far. We are gonna be having 5 tabs and calculate the width of the individual tab.

 

Remember in our first snippet we spread props in CustomTabBar <CustomTabBar {...props} /> like this. Now we are extracting onTabPress and navigation from navigation props.

 

Our component is returning the view with TabsShape and TabsHandler.

 

So let's start with TabsShape.This component is responsible for creating the curved path of our tabs and we are passing individual tabWidth and current active tab index in props.The current active tab index is required so that our SVG can be dynamic and it will redraw the curve based upon active tab.

 

In this component we will use d3-shape package to draw the SVG path.

 

Here I am not going to explain the complete functioning of how the d3-shape works, just the basic concept of it.

 

const ACTIVE_CURVE_RADIUS = 21;
const MIDDLE_CURVE_RADIUS = 50;

function TabsShape({
    tabWidth,
    index
}) {
    const d = useMemo(() => {
        const start =
            tabWidth / 2 - ACTIVE_CURVE_RADIUS + index * tabWidth + (index > 2 ? 100 - tabWidth : 0);

        const left = shape
            .line()
            .x((d) => d.x)
            .y((d) => d.y)([
                {
                    x: 0,
                    y: 0
                },
                {
                    x: start,
                    y: 0
                },
            ]);

        const tab = shape
            .line()
            .x((d) => d.x)
            .y((d) => d.y)
            .curve(shape.curveBasis)([
                {
                    x: start,
                    y: 0.3
                },
                {
                    x: start + 4,
                    y: 1
                },
                {
                    x: start + 8,
                    y: 3
                },
                {
                    x: start + 12,
                    y: 6
                },
                {
                    x: start + 18,
                    y: 10
                },
                {
                    x: start + 21,
                    y: 10.5
                },
                {
                    x: start + 24,
                    y: 10
                },
                {
                    x: start + 30,
                    y: 6
                },
                {
                    x: start + 34,
                    y: 3
                },
                {
                    x: start + 38,
                    y: 1
                },
                {
                    x: start + 42,
                    y: 0.3
                },
            ]);

        const startMiddle = wWidth / 2 - MIDDLE_CURVE_RADIUS;
        const middleTab = shape
            .line()
            .x((d) => d.x)
            .y((d) => d.y)
            .curve(shape.curveBasis)([
                {
                    x: startMiddle,
                    y: 0.3
                },
                {
                    x: startMiddle + 5,
                    y: 1
                },
                {
                    x: startMiddle + 15,
                    y: 8
                },
                {
                    x: startMiddle + 22,
                    y: 17
                },

                {
                    x: startMiddle + 32,
                    y: 28
                },

                {
                    x: startMiddle + 50,
                    y: 33
                },
                {
                    x: startMiddle + 68,
                    y: 28
                },
                {
                    x: startMiddle + 78,
                    y: 17
                },
                {
                    x: startMiddle + 85,
                    y: 8
                },
                {
                    x: startMiddle + 95,
                    y: 1
                },
                {
                    x: startMiddle + 100,
                    y: 0.3
                },
            ]);

        const right = shape
            .line()
            .x((d) => d.x)
            .y((d) => d.y)([
                {
                    x: startMiddle * 2,
                    y: 0.3
                },
                {
                    x: wWidth,
                    y: 0
                },
                {
                    x: wWidth,
                    y: NAVIGATION_BOTTOM_TABS_HEIGHT
                },
                {
                    x: 0,
                    y: NAVIGATION_BOTTOM_TABS_HEIGHT
                },
                {
                    x: 0,
                    y: 0
                },
            ]);


        return `${left} ${tab} ${middleTab} ${right}`;
    }, [tabWidth, index]);

    return (
       <Svg
        width = {
            wWidth
        }
        {
            ...{
                height: NAVIGATION_BOTTOM_TABS_HEIGHT
            }
        }
       >
        <Path fill = "white" {
            ...{
                d
            }
        }
        />
        <
        /Svg>
    );
}

 

Let's break down TabsShape component.

 

So we will first calculate the start point of our active curve which is calculated by this formula.

 

const start = tabWidth / 2 - ACTIVE_CURVE_RADIUS + index * tabWidth + (index > 2 ? 100 - tabWidth : 0);

 

Since our active curve will be at the center of the tab. So first get the center of tab by using tabWidth/2 and subtract the radius of the curve.so that is is equally distributed on left and right side.

 

Next, if the active tab is to the left of the center icon then you are good to go otherwise subtract the diameter of the middle curve where the center floating icon will be placed.

 

Believe me, this was the most difficult part. Now, all it's left is to draw the path.

 

So, the first svg path is a line upto the start point.

 

Consider the whole tab in the form of cartesian coordinates where the top left is the (0,0) point.

 

So we will start from x=0 to the start point which will be calculated from the above formula in the x-direction

 

const left = shape
        .line()
        .x((d) => d.x)
        .y((d) => d.y)([
        { x: 0, y: 0 },
        { x: start, y: 0 },
]);

 

Now, let's draw the curve.It will go on incrementing y coordinate and upon reaching centre y coordinate will decrease and x will increase throughout.

 

Now, it's time for the middle curve for the floating icon. You can all refer that from the code snippet.

 

So This TabsShape component will return the SVG path for the tab.

 

If you have reached here, then difficult part is over. Now second last step is to render the tab images and handle their onPress events.

 

This component might look scary but it is the easiest one.

 

Let's first start by seeing what this component actually returns.If you drill down to the return keyword, you will find all it does is to return an image wrapped by touchableOpacity.

 

Rest all are helper functions to dynamically get the image and show the active dot on each tab.

 

Our first helper function is a getIcon() which receives the tab as a param and we perform a simple comparison to show image for each tab and depending on the active tab (which is known by index param) we return either the selected or unselected version of it. That's it.

 

Our next helper function is to decide where to show the active tab orange dot or not. It will return true for anyone tab that is active and we absolute position it to place it at the center of the active tab curve.

 

function TabsHandler({
	routes,
	tabWidth,
	onTabPress,
	index,
	navigation
}) {
	function getIcon(tab) {
		// console.log(index, routes)
		let imageName;
		let colorName = 'orange'
		if(tab.routeName == navigationConstants.dashboard_stack) {
			if(index == 0) {
				imageName = images["dashboard_" + colorName];
			} else {
				imageName = images.dashboard_bottom;
			}
		} else if(tab.routeName == navigationConstants.leaderboard) {
			if(index == 1) {
				imageName = images["leader_" + colorName];
			} else {
				imageName = images.leaderboard_bottom;
			}
		} else if(tab.routeName == navigationConstants.notification_center) {
			if(index == 3) {
				imageName = images.notify_selected;
			} else {
				imageName = images.notification_bottom;
			}
		} else if(tab.routeName == navigationConstants.profile) {
			if(index == 4) {
				imageName = images.profile_selected;
			} else {
				imageName = images.profile_bottom;
			}
		}
		return( < Image resizeMode = "contain"
			style = {
				[{
					resizeMode: "contain"
				}]
			}
			source = {
				imageName
			}
			/>);
	}

	function renderActiveDot(tab) {
		// console.log(index, routes)
		let showDot = false;
		if(tab.routeName == navigationConstants.dashboard_stack && index == 0) {
			showDot = true;
		} else if(tab.routeName == navigationConstants.leaderboard && index == 1) {
			showDot = true;
		} else if(tab.routeName == navigationConstants.notification_center && index == 3) {
			showDot = true;
		} else if(tab.routeName == navigationConstants.profile && index == 4) {
			showDot = true;
		}
		// showDot=false
		return showDot;
	}
	return( < > {
		} < FloatingBottomIcon tabWidth = {
			tabWidth
		}
		/> < / > );
}

 

Now our final step is to show the middle floating icon at the center of the whole tab. Here it is rendered by <FloatingBottomIcon /> component. It can contain a button whereupon click you can navigate it to any screen or perform animations depending on your use case.

 

import React, { useEffect, useRef } from "react";
import { View, StyleSheet, Image, TouchableOpacity, Animated } from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { images } from "../../utils/constants/assets";
import { deviceDimensions } from "../../utils/globalFunction";
import { actionCreators } from "../../actions/actionCreators";

const wWidth = deviceDimensions().width;
const FloatingBottomIcon = (props) => {
	return( < View pointerEvents = "box-none"
		style = {
			{
				position: "absolute",
				height: 200,
				width: 200,
				bottom: 0,
				left: wWidth / 2 - 100,
				// backgroundColor:'yellow'
			}
		} > < TouchableOpacity style = {
			[{
				zIndex: showMenu ? 0 : 100
			}]
		}
		onPress = {
			spreadButtons
		} > < Image resizeMode = "contain"
		source = {
			images["start_" + colorName]
		}
		/> < /TouchableOpacity> < /View>);
};
export default FloatingBottomIcon;

 

 

"We transform your idea into reality, reach out to us to discuss it.

Or wanna join our cool team email us at [email protected] or see careers at Startxlabs."

subscribe to startxlabs

startxlabs