React Native Tabs Navigation with Expo Router
Last update: 2025-06-03Welcome! In this quick win, we’ll learn how to add a beautiful, flexible tabs navigation to your React Native app using Expo Router and React Navigation’s Tabs. We’ll cover the basics, add custom styling, and even create our own custom tab button. Let’s get started!
Galaxies-dev/
expo-router-tabs-essentials

Why Tabs Navigation?
Tabs navigation is a popular pattern for mobile apps, letting users easily switch between major sections at the bottom of the screen. With Expo Router, integrating tabs is simple and powerful, leveraging React Navigation under the hood.
1. Setting Up Your Project
Let’s start by creating a new Expo app and installing the required dependencies.
npx create-expo-app my-tabs
cd my-tabs
# remove the existing app
npm run reset-project
# Run Expo Go
npx expo
These packages give us everything we need for tabs navigation and smooth gestures.
2. Basic Tabs Layout
Let’s create a simple tabs layout. This is the foundation for your navigation structure.
import { Tabs } from 'expo-router';
export default function Layout() {
return <Tabs />;
}
To have some dummy content, let’s add this to the index.tsx
file:
import { Link } from 'expo-router';
import { Button, Image, ScrollView, Text, View } from 'react-native';
export default function Index() {
// Example to get the height of the bottom tab bar
// const height = useBottomTabBarHeight();
return (
<ScrollView>
<Link href="/profile" push asChild>
<Button title="Profile" />
</Link>
{[...Array(30)].map((_, i) => (
<View key={i} style={{ flexDirection: 'row', alignItems: 'center', padding: 24 }}>
<Image
source={require('../assets/images/react-logo.png')}
style={{ width: 32, height: 32, marginRight: 16 }}
/>
<Text style={{ fontSize: 18 }}>Dummy Item {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
Now go ahead and simply create a new file in the app
folder and add a new screen - for example, profile.tsx
.
What’s happening here?
- The
<Tabs />
component from Expo Router automatically picks up your routes and displays them as tabs at the bottom. - You can navigate between screens using the tab bar, and each screen is just a file in your
app
directory!
3. Customizing Tabs Navigation
Let’s add some style and custom screens to our tabs. You can control the look and feel, icons, and more!
Here we are testing out the screenOptions
to style the tabs (active color, badge, custom background, and more), and we also update each single tab individually by using its file name as the name of the tab!
import { HapticTab } from '@/components/HapticTab';
import { SpecialTabButton } from '@/components/SpecialTabButton';
import TabBarBackground from '@/components/TabBarBackground';
import { Ionicons } from '@expo/vector-icons';
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
export default function RootLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#ff00c3',
tabBarInactiveTintColor: '#727272',
tabBarBadgeStyle: {
backgroundColor: '#000000',
color: '#fff'
},
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: 'absolute'
},
default: {}
}),
// tabBarPosition: 'left', Moves the sidebar to the side!
// tabBarVariant: 'material',
animation: 'fade' // custom animation!
}}
>
<Tabs.Screen
name="index"
options={{
title: 'My Home',
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => <Ionicons name="home" color={color} size={size} />,
tabBarBadge: 6
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'My Profile',
tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => <Ionicons name="person" color={color} size={size} />
}}
/>
</Tabs>
);
}
The HapticTab
is a custom tab bar button that adds a soft haptic feedback when pressing down on the tabs - there is a great implementation that I’ve taken from the Expo default tabs implementation.
The TabBarBackground
is a custom tab bar background that adds a blur effect to the tab bar - this is also taken from the Expo default tabs implementation.
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
import { PlatformPressable } from '@react-navigation/elements';
import * as Haptics from 'expo-haptics';
export function HapticTab(props: BottomTabBarButtonProps) {
return (
<PlatformPressable
{...props}
onPressIn={(ev) => {
if (process.env.EXPO_OS === 'ios') {
// Add a soft haptic feedback when pressing down on the tabs.
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
props.onPressIn?.(ev);
}}
/>
);
}
Please both of these files into the components
folder.
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
export default function BlurTabBarBackground() {
return (
<BlurView
// System chrome material automatically adapts to the system's theme
// and matches the native tab bar appearance on iOS.
tint="systemChromeMaterial"
intensity={60}
style={StyleSheet.absoluteFill}
/>
);
}
export function useBottomTabOverflow() {
return useBottomTabBarHeight();
}
What’s new?
- We use
screenOptions
to style the tabs (active color, badge, custom background, and more). - Each
<Tabs.Screen />
defines a route, label, title, and icon. - The name of the screen is the name of the file!
- You can add custom tab bar buttons and backgrounds for a unique look and feel.
4. Hidden Routes and Custom Tab Buttons
Want to add dynamic items or a custom tab button? Here’s how you can do it!
You can hide certain routes from the tab bar or add a special button for custom actions.
import { HapticTab } from '@/components/HapticTab';
import { SpecialTabButton } from '@/components/SpecialTabButton';
import TabBarBackground from '@/components/TabBarBackground';
import { Ionicons } from '@expo/vector-icons';
import { Tabs } from 'expo-router';
import { Platform } from 'react-native';
export default function RootLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#ff00c3',
tabBarInactiveTintColor: '#727272',
tabBarBadgeStyle: {
backgroundColor: '#000000',
color: '#fff'
},
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: 'absolute'
},
default: {}
}),
tabBarPosition: 'left',
tabBarVariant: 'material',
animation: 'shift'
}}
>
<Tabs.Screen
name="index"
options={{
title: 'My Home',
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => <Ionicons name="home" color={color} size={size} />,
tabBarBadge: 6
}}
/>
<Tabs.Screen
name="hidden"
options={{
href: null
}}
/>
<Tabs.Screen
name="custom"
options={{
title: 'Custom',
tabBarLabel: 'Custom',
tabBarButton: SpecialTabButton
}}
listeners={{
tabPress: (e) => {
e.preventDefault();
console.log('tabPress');
}
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'My Profile',
tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => <Ionicons name="person" color={color} size={size} />
}}
/>
</Tabs>
);
}
Let’s add the special button now, which has some unique styling and a haptic feedback when pressed:
import { Ionicons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { Alert, StyleSheet, TouchableOpacity } from 'react-native';
export const SpecialTabButton = () => {
const handlePress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Alert.alert('Special Tab Button');
};
return (
<TouchableOpacity onPress={handlePress} style={styles.button} activeOpacity={0.85}>
<Ionicons name="add-circle" size={30} color="#fff" />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
position: 'absolute',
top: -20,
left: '50%',
transform: [{ translateX: -40 }],
backgroundColor: '#4F46E5',
borderRadius: 24,
width: 80,
height: 80,
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.2)'
}
});
What’s going on?
- You can hide a tab by setting
href: null
in its options. - Add a custom tab button for special actions, like opening a modal or triggering haptics.
- Use listeners to intercept tab presses and run custom logic.
5. Expo Router Unstyled Tabs
Want to build your own custom tab bar UI? Expo Router provides unstyled tab primitives for full control.
import { TabButton } from '@/components/TabButton';
import { TabList, Tabs, TabSlot, TabTrigger } from 'expo-router/ui';
export default function Layout() {
return (
<Tabs>
<TabSlot />
<TabList
style={{
backgroundColor: 'red',
padding: 20,
flexDirection: 'row',
justifyContent: 'center',
gap: 20
}}
>
<TabTrigger name="home" href="/" asChild>
<TabButton icon="home">Home</TabButton>
</TabTrigger>
<TabTrigger name="profile" href="/profile" asChild>
<TabButton icon="user">Profile</TabButton>
</TabTrigger>
</TabList>
</Tabs>
);
}
You can also find more information about this way of creating tabs on the Expo blog.
Don’t forget to add the custom TabButton
component to the components
folder for the unsyled tabs:
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { TabTriggerSlotProps } from 'expo-router/ui';
import { ComponentProps, forwardRef, Ref } from 'react';
import { Pressable, Text, View } from 'react-native';
type Icon = ComponentProps<typeof FontAwesome>['name'];
export type TabButtonProps = TabTriggerSlotProps & {
icon?: Icon;
ref: Ref<View>;
};
export const TabButton = forwardRef<View, TabButtonProps>(
({ isFocused, icon, children, ...props }, ref) => {
return (
<Pressable
{...props}
style=[
{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'column',
gap: 5,
padding: 10
},
isFocused ? { backgroundColor: 'green' } : undefined
]
>
<FontAwesome name={icon} size={24} color={isFocused ? 'white' : '#64748B'} />
<Text style={[{ fontSize: 16 }, isFocused ? { color: 'white' } : undefined]}>
{children}
</Text>
</Pressable>
);
}
);
What’s this for?
- Build a fully custom tab bar UI using Expo Router’s primitives.
- Style and arrange your tabs however you like.
Conclusion
And that’s it! You now have a fully functional, stylish tabs navigation in your Expo Router app. You can customize it, add dynamic items, and control it from anywhere in your app. This is a great foundation for any multi-page React Native project.
Want to see the full code? Check out the repo!
Happy coding! 🚀
Galaxies-dev/
expo-router-tabs-essentials
