I’m working on a custom tab bar in a React Native (Expo) application using react-native-reanimated and react-native-svg. I want to create a circular cutout effect that aligns with the tab items, but I’m struggling to achieve smooth transitions on both sides of the cutout without increasing the depth of the circle.
Current Code
import React, { useState, useEffect, useMemo } from 'react';
import { View, StyleSheet, LayoutChangeEvent, useWindowDimensions } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import Animated, {
useAnimatedStyle,
useSharedValue,
useAnimatedProps,
Easing,
withTiming,
} from 'react-native-reanimated';
import Svg, { Path } from 'react-native-svg';
import TabBarAnimation from './TabBarAnimation';
import { SafeAreaView } from 'react-native-safe-area-context';
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
const AnimatedPath = Animated.createAnimatedComponent(Path);
const CIRCLE_VERTICAL_OFFSET = -35;
const TabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
const { width } = useWindowDimensions();
const [dimensions, setDimensions] = useState({ width: width, height: 60 });
const tabWidth = dimensions.width / state.routes.length;
const buttonWidth = dimensions.width / state.routes.length;
const onTabbarLayout = (e: LayoutChangeEvent) => {
setDimensions({
height: e.nativeEvent.layout.height,
width: e.nativeEvent.layout.width,
});
};
const tabPositionX = useSharedValue(0);
const circleY = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: tabPositionX.value + 2 }, { translateY: circleY.value }],
};
});
useEffect(() => {
tabPositionX.value = withTiming(tabWidth * state.index, { duration: 300 });
}, [state.index, tabWidth]);
// This part is responsible for creating SVG
const animatedPathProps = useAnimatedProps(() => {
const cutoutCenter = tabPositionX.value + tabWidth / 2 + 2; // Center of the cutout
const cutoutRadius = 30; // Radius of the cutout
const smoothingFactor = 10; // Factor for smoothing the edges
return {
d: `
M0 0
H${cutoutCenter - cutoutRadius - smoothingFactor}
Q${cutoutCenter - cutoutRadius} 0, ${cutoutCenter - cutoutRadius} ${smoothingFactor}
A${cutoutRadius} ${cutoutRadius} 0 0 0 ${cutoutCenter + cutoutRadius} ${smoothingFactor}
Q${cutoutCenter + cutoutRadius} 0, ${cutoutCenter + cutoutRadius + smoothingFactor} 0
H${dimensions.width}
V${dimensions.height}
H0
V0
Z
`,
};
});
const circleSize = Math.min(dimensions.height * 0.8, buttonWidth * 0.8);
const circleMargin = (buttonWidth - circleSize) / 2;
return (
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<View style={styles.container}>
<View onLayout={onTabbarLayout} style={styles.tabbar}>
<Animated.View
style={[styles.circleContainer, animatedStyle, { width: tabWidth }]}
>
<View
style={[
styles.circle,
{
width: circleSize,
height: circleSize,
borderRadius: circleSize / 2,
top: `${CIRCLE_VERTICAL_OFFSET}%`,
},
]}
/>
</Animated.View>
<AnimatedSvg
width={dimensions.width}
height={dimensions.height}
style={StyleSheet.absoluteFill}
>
<AnimatedPath animatedProps={animatedPathProps} fill="black" />
</AnimatedSvg>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label: string =
typeof options.tabBarLabel === 'string'
? options.tabBarLabel
: typeof options.title === 'string'
? options.title
: route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
return (
<TabBarAnimation
key={route.name}
onPress={onPress}
onLongPress={onLongPress}
isFocused={isFocused}
routeName={route.name}
label={label}
/>
);
})}
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
tabbar: {
flexDirection: 'row',
height: 60,
},
safeArea: {
backgroundColor: 'transparent',
},
circleContainer: {
position: 'absolute',
top: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
circle: {
backgroundColor: '#000',
position: 'absolute',
},
});
export default TabBar;
Issue
When I increase the smoothingFactor
for the edges of the tab bar, it also increases the depth of the circular cutout but I want to have smooth transitions on both sides of the cutout without affecting the cutout’s depth or height if you want to call it that.
Question
What should I do to achieve smooth transitions at the edges leading to the circular cutout without changing its depth?
Desired result
This is what I want to achieve
Current State
This is what it currently looks like
Thank you for your help!