How RapidNative Generates Tab Bars, Drawers, and Bottom Sheets Automatically
(156 chars):** See how RapidNative's AI decides between tab bars, drawers, and bottom sheets — and generates each Expo Router pattern automatically from a prompt.
By Sanket Sahu
2nd Aug 2026
Last updated: 2nd Aug 2026
Navigation is where most mobile apps quietly fall apart. The screens look fine in isolation. Then you wire them together with a tab bar that has one tab, a drawer that never opens on iOS, and a bottom sheet that fights the keyboard — and suddenly the app feels amateur. Getting navigation right is one of the hardest, least-glamorous parts of building a real mobile app, and it's the part beginners routinely underestimate.
RapidNative — an AI React Native app builder that turns natural-language prompts into working Expo apps — has to make those navigation decisions automatically, on every project, without a designer in the loop. It has to pick between a tab bar, a stack, a drawer, or a modal bottom sheet based on nothing but a sentence like "a fitness tracker with a workouts screen and a progress screen." And it has to generate the right code the first time, because generated apps are exportable — they ship to the App Store, not to a design tool.
This post is a technical walkthrough of exactly how that happens: the decision tree the AI follows, the Expo Router scaffolding it emits, and the invariants that keep generated navigation from breaking.
Photo by Daniel Korpai on Unsplash
The three navigation primitives every mobile app needs
Before the AI can pick a navigation pattern, it needs to understand the same three primitives every senior mobile engineer reaches for:
- Stack — a linear push/pop history. New screens slide in from the right, back returns you to the previous screen. This is the default for any linear flow (onboarding, checkout, list → detail).
- Tab bar — a persistent bottom navigation for switching between peer-level sections that the user visits repeatedly (Feed, Search, Profile).
- Drawer — a side panel that slides in from the edge, holding sections that are less frequently used but still top-level (Settings, Help, Sign out).
- Bottom sheet / modal — a transient overlay that appears from the bottom of the screen for a focused, single-task interaction (filter picker, share sheet, quick edit).
Each of these maps to a different Expo Router construct — and picking the wrong one is one of the most common mistakes in early React Native code. A calculator app does not need a tab bar. A social app does not belong inside a stack. A filter picker should not be a full-screen route. Getting this right is what separates "I built an app in a weekend" from "this feels native."
How RapidNative decides which pattern to use
The AI mobile app builder doesn't guess. It runs a deterministic decision tree, encoded directly in its system prompt, before writing a single file.
The rule, in one line: Tabs for 3–5 peer-level sections. Stack for everything else. When in doubt, Stack.
Concretely, the AI uses tabs when the app naturally has parallel top-level sections the user switches between frequently:
- Social/community apps → Feed, Search, Profile
- E-commerce → Shop, Cart, Orders, Account
- Fitness/health → Dashboard, Workouts, Progress, Profile
It does not use tabs when the app is single-purpose or linear:
- Single-screen utilities (calculator, timer, converter, scanner)
- Step-by-step flows (onboarding, checkout, booking)
- Content-detail apps (recipe viewer, article reader)
- Simple CRUD with one main list (notes, todos, bookmarks)
Two extra guardrails prevent the most common junior-developer mistakes:
- Never render a tab bar with only one tab. If the prompt yields a single-screen app, Tabs is off the table — Stack wins by default.
- A vague prompt gets a minimal starting point. If the user says "build me a productivity app," the AI generates one screen inside a Stack, not a speculative four-tab shell it will have to rip out later.
This decision happens before code generation begins. By the time the AI writes the first _layout.tsx, the shape of the navigation has already been chosen.
Generating a tab bar: the Expo Router scaffold
When the AI decides on tabs, it generates a specific file structure that uses Expo Router's file-system routing to define the tab bar declaratively. Here is the actual layout the scaffold emits, adapted from the RapidNative Expo template:
// app/(app)/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { HomeIcon, DumbbellIcon, TrendingUpIcon, UserIcon } from 'lucide-react-native';
import { cssInterop, useColorScheme } from 'nativewind';
cssInterop(HomeIcon, { className: { target: 'style', nativeStyleToProp: { color: true } } });
cssInterop(DumbbellIcon, { className: { target: 'style', nativeStyleToProp: { color: true } } });
cssInterop(TrendingUpIcon, { className: { target: 'style', nativeStyleToProp: { color: true } } });
cssInterop(UserIcon, { className: { target: 'style', nativeStyleToProp: { color: true } } });
export default function TabsLayout() {
const { colorScheme } = useColorScheme();
const isDark = colorScheme === 'dark';
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: isDark ? '#0b0b0f' : '#ffffff',
borderTopColor: isDark ? '#1f1f24' : '#e5e7eb',
},
tabBarActiveTintColor: isDark ? '#f5f5f7' : '#111827',
tabBarInactiveTintColor: isDark ? '#71717a' : '#9ca3af',
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ focused }) => (
<HomeIcon className={focused ? 'text-primary' : 'text-muted-foreground'} size={24} />
),
}}
/>
<Tabs.Screen
name="workouts"
options={{
title: 'Workouts',
tabBarIcon: ({ focused }) => (
<DumbbellIcon className={focused ? 'text-primary' : 'text-muted-foreground'} size={24} />
),
}}
/>
{/* Progress + Profile screens follow the same pattern */}
</Tabs>
);
}
Several things are going on here that only make sense once you understand the constraints the AI is designed around.
File-system routing means the file is the route. The (tabs) folder is an Expo Router group — the parentheses tell the router to treat it as a layout boundary, not a URL segment. Every .tsx file inside (tabs)/ becomes a tab, and the _layout.tsx at the group root declares the tab bar. That means adding a fifth tab is literally move_file a screen into the folder and register it with <Tabs.Screen> — no route table to edit, no manual wiring.
Icons come from lucide-react-native — nothing else. The AI is only permitted to import icons that exist in that library. This is a hard constraint: prior versions of the system used to hallucinate icons like MessageIcon or RecordIcon, which crash at runtime because they don't exist. The current prompt bans that class of error by pinning icons to a known-good set.
cssInterop bridges NativeWind and Lucide. By default, Lucide's color prop is a hex string. NativeWind (Tailwind for React Native) styles via className. The cssInterop call teaches Lucide to accept className="text-primary" and derive the color from the theme, which keeps the tab bar consistent with the rest of the app in both light and dark mode.
Tab bar colors are derived, not hardcoded arbitrarily. The tabBarStyle, tabBarActiveTintColor, and tabBarInactiveTintColor all resolve to hex values pulled from the project's theme.ts — the same source of truth every other screen uses. There's a subtle reason for this: tabBarStyle on React Navigation only accepts primitive style values, not Tailwind classes, so the AI has to convert its RGB theme tokens to hex at generation time. The isDark ternary is the escape hatch that keeps that conversion honest across color schemes.
The (tabs) group vs. the (app) group: two layout boundaries
A subtle but important detail: the scaffold has two nested route groups.
app/
_layout.tsx // Root providers (theme, safe area, auth)
(app)/
_layout.tsx // Authenticated area — Stack by default
(tabs)/
_layout.tsx // Tab bar
index.tsx // Home
workouts.tsx // Workouts
progress.tsx // Progress
profile.tsx // Profile
tickets/
[ticketId].tsx // Detail route pushed on top of the tab bar
The outer (app) group is a Stack. The inner (tabs) group is a Tabs layout. This means when a user taps a workout in the list and pushes into a detail screen, the tab bar naturally stays below the pushed screen because the Tabs layout is nested inside the Stack. Detail screens defined at (app)/tickets/[ticketId].tsx — outside the (tabs) folder — push over the entire tab UI, hiding the bar on the detail view. This is the standard Expo Router pattern and it works because layouts compose top-down.
Getting this nesting right by hand takes people two or three failed attempts. Generating it correctly from a prompt requires the AI to understand not just what pattern to use, but where in the layout tree each piece belongs.
Generating a drawer: when tabs aren't enough
Drawers are less common than tab bars in modern mobile apps — they've largely been replaced by bottom navigation on iOS and Android — but they still shine when you have 6+ top-level sections, an admin app with a lot of surface area, or an internal tool. The RapidNative scaffold ships with a custom drawer implementation at tools/rapidnative-expo-router/src/components/Drawer.tsx that works consistently across web preview and native builds.
The drawer exposes a small, familiar context API:
interface DrawerContextValue {
isOpen: boolean;
openDrawer: () => void;
closeDrawer: () => void;
toggleDrawer: () => void;
}
And child screens are registered with Drawer.Screen entries that mirror the Tabs API:
<Drawer>
<Drawer.Screen name="dashboard" options={{ drawerLabel: 'Dashboard', drawerIcon: HomeIcon }} />
<Drawer.Screen name="reports" options={{ drawerLabel: 'Reports', drawerIcon: FileTextIcon }} />
<Drawer.Screen name="settings" options={{ drawerLabel: 'Settings', drawerIcon: SettingsIcon }} />
</Drawer>
The AI reaches for a drawer only when the app has many peer-level sections that don't fit in a 5-tab bar. For an app with three sections, tabs win. For an app with eight, the drawer wins. In between — say, four sections plus a Settings screen — the AI usually generates a 4-tab bar plus Settings buried inside the Profile tab, because that's what production apps like Instagram and TikTok actually do.
Generating a bottom sheet: modals done right
Bottom sheets are the third pattern, and they're the one most beginners get wrong. A bottom sheet is not a route. It's a transient UI that overlays the current screen for a focused task — picking a filter, confirming an action, quickly editing a field — and it dismisses back to where you came from. Routing it as a full screen breaks the mental model: users expect the underlying screen to still exist.
RapidNative's default bottom sheet uses React Native's built-in Modal component with transparent and animationType="slide", following the pattern documented in its layout guide:
<Modal
visible={isOpen}
transparent
animationType="slide"
onRequestClose={onClose}
>
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}>
<View className="mt-auto bg-card rounded-t-3xl px-5 pt-6 pb-10">
<View className="w-10 h-1 rounded-full bg-muted self-center mb-4" />
{/* Sheet content */}
</View>
</View>
</Modal>
There are three deliberate constraints in that snippet:
- The overlay is the only place inline
rgbacolors are allowed in the generated code. Everywhere else, colors come from the theme. Overlays are a semi-transparent scrim, which doesn't map cleanly to a semantic color token, so the rule carves out a single exception rather than inventing a token for it. - The sheet is a
bg-card rounded-t-3xlcontainer pinned to the bottom withmt-auto— the pattern is defined once and reused across every generated sheet. - The drag handle at the top (
w-10 h-1 rounded-full bg-muted) is a stylistic promise to the user. It signals "you can drag me down" even when the actual gesture is handled by the close button, because users have been trained by iOS and Android to look for it.
For projects that need a heavier bottom sheet — snap points, backdrop taps, keyboard-aware layout — the scaffold's dependency graph includes @gorhom/bottom-sheet 5.0.0-alpha.11, so the AI can upgrade the primitive when the prompt asks for it. But the default sticks with the built-in Modal because it works everywhere, has no gesture-handler edge cases, and doesn't add bundle size for apps that don't need it.
Transformations: converting a Stack app into a Tabs app mid-conversation
Here's the harder problem. What happens when a user starts with "a note-taking app" — which the AI correctly generates as a single-screen Stack — and then, five messages later, says "actually, add a search tab and a settings tab"?
The AI can't just start over. The user has been iterating on the notes screen; that work has to survive. So RapidNative uses a move_file operation to relocate existing screens into a new (tabs) group without rewriting them:
BEFORE:
app/(app)/index.tsx // Notes list
AFTER (single move_file + one new layout):
app/(app)/(tabs)/_layout.tsx // NEW — declares the tab bar
app/(app)/(tabs)/index.tsx // MOVED from (app)/index.tsx
app/(app)/(tabs)/search.tsx // NEW
app/(app)/(tabs)/settings.tsx // NEW
The existing notes screen file is unchanged — the AI is explicitly instructed not to rewrite the screen when it moves it. Only the layout above it changes. This is the difference between an AI that regenerates from scratch on every prompt (destroying user edits) and an AI that understands its edits are additive.
The reverse transformation — converting Tabs → Stack — works the same way in reverse, though it's rarer. It happens when a user prunes their app back down to a single flow.
Guardrails: the invariants that keep generated navigation from breaking
The specific rules aren't fancy, but each one exists because of a specific class of failure the earlier system hit:
- Never render a tab bar with a single tab. A one-tab tab bar wastes 60px of screen real estate and confuses users. If the app is single-purpose, the AI must use Stack.
- Tab bar screens use
edges={['top']}onSafeAreaView, not['top', 'bottom']. The tab bar itself is already inset from the bottom; adding a bottom safe-area on the screen doubles the padding and creates a visible gap. - Detail screens are dynamic routes, not modal overlays with props. A tapped item routes to
[ticketId].tsx, not a stateful modal — this makes deep links, refresh, and back-navigation work correctly. tabBarStyleaccepts onlybackgroundColorandborderTopColor. Height, padding, and shadow are off-limits because they interact badly with iOS home-indicator inset math and produce a visibly-broken tab bar on notched devices.- Icons must exist in
lucide-react-native. If the AI needs an icon whose name it can't verify, it picks the closest real one. This eliminates the entire class of "import cannot be resolved" runtime crashes.
Each invariant looks small in isolation. Together they're the difference between generated code that runs on a real device and generated code that only survives the preview.
The dependency stack under the hood
For readers who care about versions, the navigation stack the scaffold ships with:
expo-router6.0.12 — file-system routing on top of React Navigation@react-navigation/native7.1.18 and@react-navigation/bottom-tabs7.4.9 — the underlying navigation enginereact-native-screens4.16.0 — native screen containers for performancereact-native-gesture-handler2.28.0 — required for drawer swipes and sheet dragsreact-native-safe-area-context5.6.2 — safe-area insets, used everywhere the tab bar or bottom sheet touches the screen edgelucide-react-native0.510.0 — the icon set the AI is allowed to import from@gorhom/bottom-sheet5.0.0-alpha.11 — heavier bottom-sheet primitive, opt-in for complex sheets
This is a deliberately boring stack. Every dependency is either from the Expo core team or a well-maintained community library. There's nothing custom about the primitives — the intelligence lives in the decision tree and the invariants, not in a bespoke navigation framework.
Why this matters if you're building with AI
The pattern here generalizes past navigation. Everywhere an AI code generator has to make a design decision that a human would make automatically — which font to use for a title, whether to add a loading state, whether an input field should be numeric — the same three ingredients apply: a decision tree encoded in the system prompt, a small library of canonical templates the model can emit, and a set of hard invariants that block the whole class of common mistakes.
RapidNative applies that pattern to navigation because navigation is the highest-stakes visible surface in a mobile app. Get it right and the app feels native. Get it wrong and no amount of polish on the individual screens will save the impression.
If you want to see the decision tree in action, describe a mobile app in one sentence and generate it. The tab bar or stack you get back is the direct output of this process — and you can watch RapidNative pick between them in real time based on what your prompt actually implies. For more on how the generated code hangs together, see how RapidNative handles navigation and routing and the broader piece on React Native navigation patterns for multi-screen apps.
For deeper reference on the underlying primitives, the Expo Router documentation and the React Navigation bottom tabs guide are the canonical sources — RapidNative's scaffold is a specific opinionated arrangement of exactly those APIs, chosen so an AI can generate correct navigation on the first try.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.