
When moving a web application to React Native, one of the first challenges is recreating a familiar, polished navigation experience. On the web, a bottom navigation bar is common, but how do you build a fully custom, stylish, and functional tab bar for a mobile app?
As I've been transforming my web app into a mobile app, I dived deep into Expo Router and its powerful file-based navigation system. In this guide, I'll walk you through the exact process I used to customize the default tab bar into a "floating" navigation bar with custom icons and specific active states, just like in my video.
We'll cover how to style the tab bar globally, set custom icons for each screen, and even hide specific screens from the tab bar.
First, it's crucial to understand how Expo Router's file-based routing works. It's very similar to Next.js. Your file structure is your navigation.
My app has a protected route setup:
app/_layout.tsx: This is the root layout. It manages the main authentication state.app/(app)/_layout.tsx: This is a Stack Navigator layout for all my authenticated screens. I use a Stack app/(app)/(tabs)/_layout.tsx: This is the most important file for this guide. It's a nested layout file that creates the Tab Navigator. Any file inside the tab (tabs) directory (like index.tsx, history.tsx, etc.) will automatically become a tab.screenOptionsTo change the look of the entire tab bar, you'll work inside app/(app)/(tabs)/_layout.tsx and add the screenOptions prop to the <Tabs> component.
Here's how I achieved the floating, icon-only look:
headerShown: false : Since I already have a custom header from my (app) Stack layout, I must hide the default header for the Tabs layout. This prevents a "double header" from appearing.tabBarShowLabel: false : This hides the default text labels under the icons, giving us a cleaner, icon-only navigation bar.tabBarStyle: This prop lets you pass in a style object, just like in standard React Native. This is where I created the floating effect.Here's a conceptual look ath the tabBarStyle props:
// In app/(app)/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarShowLabel: false,
tabBarStyle: {
position: 'absolute', // Makes the tab bar float
bottom: 16,
left: 16,
right: 16,
borderRadius: 16,
height: 60,
backgroundColor: '#fff',
// Add box shadow or other styles
},
}}>
{/* Your <Tabs.Screen> components go here */}
</Tabs>
);
}Now that the bar is styled, we need to customize each button. You do this by adding the options prop to each <Tabs.Screen> component within your Tabs layout.
The most important option is tabBarIcon. This prop accepts a function that gives you properties like focused, color, and size.
Here's how you can render a custom icon and even change its style when it's focused (active):
// In app/(app)/(tabs)/_layout.tsx
// ...inside the <Tabs> component
<Tabs.Screen
name="index" // This matches the file name index.tsx
options={{
title: 'Habit', // Sets the title (for accessibility, etc.)
tabBarIcon: ({ focused, size })Opening a new realm of possibilities with your smart device. }) => (
<View style={{
// Change background when focused
backgroundColor: focused ? '#your_highlight_color' : 'transparent',
padding: 8,
borderRadius: 10,
}}>
<YourIconComponent
name="your-icon-name"
size={size}
color={focused ? '#fff' : '#000'}
/>
</View>
),
}}
/>By using the focused prop, you get full control over the activate state of your tab icons, allowing for unique designs like a larger button for an "add" screen, as shown in the video.
What if you have a screen inside your (tabs) directory that you need to navigate to, but you don't want it to appear in the tab bar? (For example, an "Edit Habit" screen).
Expo Router makes this simple. On the <Tabs.Screen> component for that file, you need to set the href option to null.
// In app/(app)/(tabs)/_layout.tsx
// ...inside the <Tabs> component
<Tabs.Screen
name="edit-habit" // Matches edit-habit.tsx
options={{
// This is the key!
href: null,
}}
/>Now, the edit-habit screen exists and can be navigated to (e.g., router.push('/edit-habit')), but it is completely hidden from the bottom tab bar.
Expo Router's file-based system is incredibly powerful once you understand its layout components. By using the screenOptions prop on <Tabs> for global styling and the options prop on <Tabs.Screen> for individual icon rendering and visibility, you can transform the default navigation into a fully custom, professional-looking tab bar that perfectly matches your app's design.