React Native Intermediate: Navigation, Data and Device Features
Tabs and Layouts
In this lesson you will
- Create a tab bar with a route group and a Tabs layout
- Nest a stack inside a tab and present a screen as a modal
- Keep content clear of the notch and home indicator with safe area insets
Introduction
Open almost any app and you will see a bar of tabs at the bottom: Home, Search, Profile. This lesson builds one with Expo Router, then shows how to put a stack inside a tab, open a screen as a pop-up modal, and keep content from sliding under the phone's notch.
Route groups
A folder name in round brackets, such as (tabs), is a route group. It organises files without becoming part of the address. The docs give the example: src/app/(tabs)/index.tsx can serve the address /, because the group name is left out.
Here is a typical layout for an app with two tabs:
src/app/
_layout.tsx the root: a stack that holds the tabs
(tabs)/
_layout.tsx the tab bar
index.tsx the Home tab, at "/"
settings.tsx the Settings tab, at "/settings"
modal.tsx a screen that slides up over everythingQuick quiz
Does the name (tabs) appear in the address of the screens inside it?
The tab bar
The layout inside (tabs) renders a Tabs component. Each Tabs.Screen names a file and describes its tab:
React (TSX)
import Ionicons from "@expo/vector-icons/Ionicons";
import { Tabs } from "expo-router";
export default function TabsLayout() {
return (
<Tabs screenOptions={{ tabBarActiveTintColor: "#5b4bf5" }}>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color as string} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color, size }) => <Ionicons name="settings" size={size} color={color as string} />,
}}
/>
</Tabs>
);
}screenOptions applies to every tab, here the highlight colour. tabBarIcon receives the color and size the tab bar wants, so the icon automatically turns the active colour. Icons come from @expo/vector-icons, which is included in a standard Expo project. TypeScript types color as any possible colour value, while Ionicons wants a string, so the code says color as string. At run time the tab bar passes an ordinary string.
A note on what you see in a brand new project: the default template may build its tabs in a different, more native way. Check the project's own _layout.tsx and the Expo docs. The Tabs component above works in every project, and the ideas (one file per tab, options per screen) are the same.
The root layout holds the tabs
The root _layout.tsx is a stack, and one of its screens is the whole tab group:
React (TSX)
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: "modal", title: "New note" }} />
</Stack>
);
}headerShown: falsehides the stack's header for the tab group, because the tabs draw their own headers.presentation: "modal"makes themodalscreen slide up over the tabs as a sheet instead of pushing sideways. It is perfect for "create something" screens.
Open the modal from any tab with router.push("/modal") or a Link, and close it with router.back().
React (TSX)
import { Link } from "expo-router";
import { StyleSheet, Text, View } from "react-native";
export default function Home() {
return (
<View style={styles.screen}>
<Text style={styles.title}>Notes</Text>
<Link href="/modal" style={styles.link}>
Write a new note
</Link>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 },
title: { fontSize: 28, fontWeight: "bold" },
link: { fontSize: 18, color: "#5b4bf5" },
});A stack inside a tab
Often one tab needs its own back-and-forth navigation, for example Home leading to a detail screen while the tab bar stays visible. Make the tab a folder with its own layout:
src/app/(tabs)/
_layout.tsx
home/
_layout.tsx a Stack for this tab only
index.tsx the list
[id].tsx the detail screen
settings.tsxReact (TSX)
import { Stack } from "expo-router";
export default function HomeStack() {
return <Stack />;
}In the tab layout, the tab is now named home (the folder). Detail screens push inside this tab and the tab bar stays at the bottom.
Quick quiz
Which option makes a screen slide up over the tabs as a sheet?
Safe areas
Modern phones have notches, rounded corners and a home indicator bar. Content drawn under them can be cut off or hard to tap. These protected zones are called safe areas.
Navigation headers and tab bars already respect them. For your own full-screen content, use the react-native-safe-area-context package, which Expo Router installs. React Native's built-in SafeAreaView is deprecated, and the official docs point to this package instead.
React (TSX)
import { StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function Splash() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.screen, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<Text style={styles.title}>Welcome</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#0b1020", justifyContent: "center", alignItems: "center" },
title: { color: "white", fontSize: 28 },
});useSafeAreaInsets returns the size of each protected edge in pixels (top, bottom, left, right), and you add that much padding. The values are zero on a phone without a notch, so one piece of code works everywhere.
Try it yourself
Create two tabs, Home and Settings, with icons. Add a modal screen opened from Home with a "Close" button that calls router.back(). Use useSafeAreaInsets to pad a full-screen screen, and test on a phone or emulator with a notch.
Summary
- A folder in round brackets, like
(tabs), is a route group: it shares a layout and stays out of the address. - A
Tabslayout draws the tab bar. EachTabs.Screenconfigures one tab with a title and icon, and a tab can be a folder with its ownStack. - Present a screen as a sheet with
presentation: "modal", and useuseSafeAreaInsetsfromreact-native-safe-area-contextto keep content clear of notches.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading