@psync/curved-bottom-bar
v1.0.0
Published
A high performance, beautiful and fully customizable curved bottom navigation bar for React Native.
Maintainers
Readme

@psync/curved-bottom-bar
A high performance, beautiful and fully customizable curved bottom navigation bar for React Native. Implemented using react-native-svg and @react-navigation/bottom-tabs.
If you love this library, give us a star, you will be a ray of sunshine in our lives :)Free React Native Boilerplate
React Native Template with a beautiful UI.
Getting started
npm install @psync/curved-bottom-bar --saveor
bun add @psync/curved-bottom-barNow we need to install the required peer dependencies:
npm install @react-navigation/native @react-navigation/bottom-tabs react-native-screens react-native-safe-area-context react-native-svgor
yarn add @react-navigation/native @react-navigation/bottom-tabs react-native-screens react-native-safe-area-context react-native-svgReact Navigation v7 & v8 — this library works with both
@react-navigation/bottom-tabsv7 and the v8 pre-release.
- v7 requires
@react-navigation/native,react-native-screens ≥ 4, andreact-native-safe-area-context ≥ 4.- v8 raises the minimum versions:
react ≥ 19.2,react-native ≥ 0.83(New Architecture only),react-native-screens ≥ 4.25, andreact-native-safe-area-context ≥ 5.5.The curved bar always renders through React Navigation's JavaScript-based tab bar, so the visual result is identical on both versions. See React Navigation v7 vs v8 below for the details. Follow the React Navigation getting started guide for additional native setup steps.
Demo

CurvedBottomBar.Navigator
| Props | Params | isRequire | Description |
|--------------------|-----------------------------------------------------------------|-----------|-------------------------------------------------------------------------------------|
| initialRouteName | String | Yes | The name of the route to render on first load of the navigator |
| renderCircle | ({ routeName, selectedTab, navigate }) => React.ReactElement | Yes | Function that returns a React element to display as the center tab item |
| type | 'DOWN' or 'UP' | No | Type of the center tab item, downward curve or upward curve. Default: 'DOWN' |
| circlePosition | 'CENTER' or 'LEFT' or 'RIGHT' | No | Position of circle button. Default: 'CENTER' |
| tabBar | ({ routeName, selectedTab, navigate }) => React.ReactElement | No | Function that returns a React element to display as a tab bar icon |
| circleWidth | Number | No | Customize width of the center tab item. Minimum is 50px and Maximum is 60px |
| style | ViewStyle | No | Styling for container view |
| shadowStyle | ViewStyle | No | Styling for shadow view. Not supported in Expo — use CurvedBottomBarExpo instead |
| width | Number | No | Customize width for container view |
| height | Number | No | Customize height for container view. Minimum is 50px and Maximum is 90px |
| borderTopLeftRight | Boolean | No | Border radius top left and top right of container view |
| borderColor | String | No | Border color |
| borderWidth | Number | No | Border width |
| bgColor | String | No | Background color of container view |
| id | String | No | Optional navigator ID. React Navigation v7 only — removed in v8 (ignored). |
| screenOptions | BottomTabNavigationOptions or function | No | Default options for all screens |
| backBehavior | 'firstRoute' | 'initialRoute' | 'order' | 'history' | 'none' | No | Behaviour of back button. Default: 'firstRoute' |
| implementation | 'custom' | 'native' | No | Tab bar rendering implementation. Default: 'custom'. Only honored by React Navigation v8; ignored by v7. Keep 'custom' to render the curved bar. |
| enableGlassEffect | Boolean | No | Opt-in to Apple's Liquid Glass material on iOS 26+. See "Liquid Glass" below. |
| glassEffectStyle | 'regular' | 'clear' | No | Liquid Glass material variant. Default: 'regular' |
CurvedBottomBar.Screen
| Props | Params | isRequire | Description |
|-----------|---------------------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | String | Yes | Name of the route |
| position | 'LEFT' or 'RIGHT' or 'CIRCLE' or 'CENTER' | Yes | Position of the tab icon. Use 'CIRCLE' or 'CENTER' when the circle button should itself be a navigable tab view |
| component | React.ComponentType | Yes | Component to render for this screen |
| options | BottomTabNavigationOptions | No | Screen-level navigation options |
API
| Function | Params | Description | | ------------------ | ----------------------------- | ----------------------------------------------------------------------------------------- | | setVisible | Boolean | Used to hide/show the tab bar. Ex: ref.current.setVisible(false) |
React Navigation v7 vs v8
This library is compatible with both @react-navigation/bottom-tabs v7 and
the v8 pre-release. The peer dependency range allows
^7.0.0 || ^8.0.0-alpha.
Compatibility is achieved without breaking v7. The single behavioural change
v8 introduces for this library is that v8 renders native bottom tabs by
default, which would bypass the custom curved SVG tab bar. To prevent that, the
navigator always passes implementation="custom" to React Navigation, which
forces the JavaScript-based tab bar on v8. The implementation prop does not
exist in v7 and is simply ignored there, so existing v7 apps are unaffected
and the curved bar looks identical on both versions.
Things to be aware of when running on v8:
- Minimum versions are higher. v8 requires
react ≥ 19.2,react-native ≥ 0.83(New Architecture only — the old architecture is no longer supported),react-native-screens ≥ 4.25, andreact-native-safe-area-context ≥ 5.5. If you cannot meet these, stay on v7. - The
idprop is removed in v8. React Navigation v8 dropped the navigatoridprop (use a parent screen name withnavigation.getParent('ScreenName')instead). On v7 theidprop still works; on v8 it is ignored. liquid glassmaterial. v8 ships its own iOS 26 liquid-glass support via@callstack/liquid-glass. This library's separateenableGlassEffectprop (backed byexpo-glass-effect) remains independent and optional.
If you do not pass implementation, it defaults to 'custom', which is the
correct value for the curved bar on both v7 and v8. Passing
implementation="native" on v8 will render React Navigation's native tab bar
and bypass the curved design, so only do that intentionally.
Use in Expo

import React from 'react';
import {
Alert,
Animated,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
import { CurvedBottomBarExpo } from '@psync/curved-bottom-bar';
import Ionicons from '@expo/vector-icons/Ionicons';
import { NavigationContainer } from '@react-navigation/native';
const Screen1 = () => {
return <View style={styles.screen1} />;
};
const Screen2 = () => {
return <View style={styles.screen2} />;
};
export default function App() {
const _renderIcon = (routeName, selectedTab) => {
let icon = '';
switch (routeName) {
case 'title1':
icon = 'ios-home-outline';
break;
case 'title2':
icon = 'settings-outline';
break;
}
return (
<Ionicons
name={icon}
size={25}
color={routeName === selectedTab ? 'black' : 'gray'}
/>
);
};
const renderTabBar = ({ routeName, selectedTab, navigate }) => {
return (
<TouchableOpacity
onPress={() => navigate(routeName)}
style={styles.tabbarItem}
>
{_renderIcon(routeName, selectedTab)}
</TouchableOpacity>
);
};
return (
<NavigationContainer>
<CurvedBottomBarExpo.Navigator
type="DOWN"
style={styles.bottomBar}
shadowStyle={styles.shawdow}
height={55}
circleWidth={50}
bgColor="white"
initialRouteName="title1"
borderTopLeftRight
renderCircle={({ selectedTab, navigate }) => (
<Animated.View style={styles.btnCircleUp}>
<TouchableOpacity
style={styles.button}
onPress={() => Alert.alert('Click Action')}
>
<Ionicons name={'apps-sharp'} color="gray" size={25} />
</TouchableOpacity>
</Animated.View>
)}
tabBar={renderTabBar}
>
<CurvedBottomBarExpo.Screen
name="title1"
position="LEFT"
component={() => <Screen1 />}
/>
<CurvedBottomBarExpo.Screen
name="title2"
component={() => <Screen2 />}
position="RIGHT"
/>
</CurvedBottomBarExpo.Navigator>
</NavigationContainer>
);
}
export const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
shawdow: {
shadowColor: '#DDDDDD',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 1,
shadowRadius: 5,
},
button: {
flex: 1,
justifyContent: 'center',
},
bottomBar: {},
btnCircleUp: {
width: 60,
height: 60,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E8E8E8',
bottom: 30,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.2,
shadowRadius: 1.41,
elevation: 1,
},
imgCircle: {
width: 30,
height: 30,
tintColor: 'gray',
},
tabbarItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
img: {
width: 30,
height: 30,
},
screen1: {
flex: 1,
backgroundColor: '#BFEFFF',
},
screen2: {
flex: 1,
backgroundColor: '#FFEBCD',
},
});
Use in RN CLI

import React from 'react';
import {
Alert,
Animated,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
import { CurvedBottomBar } from '@psync/curved-bottom-bar';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { NavigationContainer } from '@react-navigation/native';
const Screen1 = () => {
return <View style={styles.screen1} />;
};
const Screen2 = () => {
return <View style={styles.screen2} />;
};
export default function App() {
const _renderIcon = (routeName, selectedTab) => {
let icon = '';
switch (routeName) {
case 'title1':
icon = 'ios-home-outline';
break;
case 'title2':
icon = 'settings-outline';
break;
}
return (
<Ionicons
name={icon}
size={25}
color={routeName === selectedTab ? 'black' : 'gray'}
/>
);
};
const renderTabBar = ({ routeName, selectedTab, navigate }) => {
return (
<TouchableOpacity
onPress={() => navigate(routeName)}
style={styles.tabbarItem}
>
{_renderIcon(routeName, selectedTab)}
</TouchableOpacity>
);
};
return (
<NavigationContainer>
<CurvedBottomBar.Navigator
type="UP"
style={styles.bottomBar}
shadowStyle={styles.shawdow}
height={55}
circleWidth={50}
bgColor="white"
initialRouteName="title1"
borderTopLeftRight
renderCircle={({ selectedTab, navigate }) => (
<Animated.View style={styles.btnCircleUp}>
<TouchableOpacity
style={styles.button}
onPress={() => Alert.alert('Click Action')}
>
<Ionicons name={'apps-sharp'} color="gray" size={25} />
</TouchableOpacity>
</Animated.View>
)}
tabBar={renderTabBar}
>
<CurvedBottomBar.Screen
name="title1"
position="LEFT"
component={() => <Screen1 />}
/>
<CurvedBottomBar.Screen
name="title2"
component={() => <Screen2 />}
position="RIGHT"
/>
</CurvedBottomBar.Navigator>
</NavigationContainer>
);
}
export const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
shawdow: {
shadowColor: '#DDDDDD',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 1,
shadowRadius: 5,
},
button: {
flex: 1,
justifyContent: 'center',
},
bottomBar: {},
btnCircleUp: {
width: 60,
height: 60,
borderRadius: 30,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E8E8E8',
bottom: 18,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.2,
shadowRadius: 1.41,
elevation: 1,
},
imgCircle: {
width: 30,
height: 30,
tintColor: 'gray',
},
tabbarItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
img: {
width: 30,
height: 30,
},
screen1: {
flex: 1,
backgroundColor: '#BFEFFF',
},
screen2: {
flex: 1,
backgroundColor: '#FFEBCD',
},
});
Use with expo-router
This package ships an optional entry point for expo-router.
expo-router is not a hard dependency — it is loaded dynamically and only
required if you import the entry below.
// app/(tabs)/_layout.tsx
import { CurvedTabs } from '@psync/curved-bottom-bar/expo-router';
import { Ionicons } from '@expo/vector-icons';
import { TouchableOpacity } from 'react-native';
export default function TabLayout() {
return (
<CurvedTabs
initialRouteName="home"
type="DOWN"
bgColor="white"
height={60}
circleWidth={50}
renderCircle={({ navigate }) => (
<TouchableOpacity onPress={() => navigate('camera')}>
<Ionicons name="apps-sharp" size={25} color="gray" />
</TouchableOpacity>
)}
tabBar={({ routeName, selectedTab, navigate }) => (
<TouchableOpacity onPress={() => navigate(routeName)}>
<Ionicons
name={routeName === 'home' ? 'home-outline' : 'settings-outline'}
size={25}
color={routeName === selectedTab ? 'black' : 'gray'}
/>
</TouchableOpacity>
)}
>
<CurvedTabs.Screen name="home" position="LEFT" />
<CurvedTabs.Screen name="camera" position="CIRCLE" />
<CurvedTabs.Screen name="settings" position="RIGHT" />
</CurvedTabs>
);
}Liquid Glass (iOS 26+)
Apple introduced the Liquid Glass material in iOS 26. Pass
enableGlassEffect on the navigator to opt in. The library uses the optional
peer dependency expo-glass-effect
to render the material; on Android, on iOS < 26, or when expo-glass-effect
is not installed the prop is a no-op and the bar renders normally.
npx expo install expo-glass-effect<CurvedBottomBarExpo.Navigator
enableGlassEffect
glassEffectStyle="regular" // or 'clear'
bgColor="transparent" // important: do not occlude the glass material
// ...other props
/>