A working bottom tab bar feels great, but the moment you tweak the code Rork generates, the icons sometimes disappear or tabs refuse to switch. After shipping a few Rork apps, I noticed I was hitting the same bug at least three times — that's why I sat down and turned my recovery process into this guide.
The painful thing about this bug is that knowing where to look first saves you half a day. So before you start editing _layout.tsx at random, sort the symptom into one of three buckets and work through the matching fix. The diagnosis takes about a minute and the rest of this article is structured around acting on that diagnosis.
Triage the symptom into one of three patterns
Rork's bottom tab issues almost always fall into these categories:
- Pattern A: Icons don't render, but tabs still switch when tapped.
- Pattern B: Icons render fine, but tapping a tab does nothing.
- Pattern C: Everything works in development, but icons vanish in production builds.
A and B have completely different root causes, so trying the same fix on both will waste your time. Open the app in Expo Go, Rork Companion, or your simulator and confirm which bucket you're in. Tap each tab. Look at the icons. Then jump straight to the matching section below.
If you're working with someone else's report ("the tabs are broken"), make them send a screenshot first. Half the time, the screenshot alone tells you which pattern it is and you can skip a back-and-forth.
Pattern A: Icons don't render
In my experience this almost always comes down to either the return type of tabBarIcon or a missing icon library dependency. Rork's generated code typically uses lucide-react-native, and swapping it out for another library (or modifying the icon function signature) is where most regressions sneak in.
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Home, Settings, User } from "lucide-react-native";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
// BAD: passing a JSX element instead of a function
// means size/color never reach the icon, and the
// active/inactive tint stops working.
// tabBarIcon: <Home />,
// GOOD: ({ color, size }) => ReactNode
tabBarActiveTintColor: "#0070f3",
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: "Settings",
tabBarIcon: ({ color, size }) => <Settings color={color} size={size} />,
}}
/>
</Tabs>
);
}Three things to verify: (1) tabBarIcon must be a function, not a pre-rendered JSX element. The function shape ({ color, size, focused }) => ReactNode is what react-navigation hands the active tint to. (2) The function must forward color and size so the active/inactive state and tab bar height can drive the icon's appearance. If you hard-code a color, the focused state stops being visible. (3) If you're using lucide-react-native, you also need react-native-svg installed — without it, the icons silently fail without throwing an error. A quick expo install react-native-svg followed by a fresh build usually brings them back instantly.
One subtle gotcha: if you upgrade lucide-react-native independently of the rest of the toolchain, you can land on a major version that requires a newer react-native-svg than what's pinned in your package.json. Run expo doctor after any icon-library upgrade — it catches this almost every time.
Pattern B: Tabs don't switch when tapped
If the tabs render but tapping them does nothing, suspect a mismatch between the route name in <Tabs.Screen name="..." /> and the actual file name in app/(tabs)/. expo-router maps file names directly to routes, so name="profile" paired with a file called Profile.tsx (capital P) will register as a non-matching route and the tap becomes a no-op. macOS and Linux file systems handle case differently, which is why this often passes locally and breaks in CI.
The fix flow is straightforward: list the contents of app/(tabs)/ and confirm every filename is lowercase with hyphens, matching the name= you pass to Tabs.Screen. While you're there, double-check that the order of <Tabs.Screen /> declarations doesn't accidentally skip a file. expo-router will route to a file even if it's not declared in _layout.tsx, but it can't fall back to a missing one.
The other common offender is nested stacks inside a tab. If you put a Stack inside a tab folder, you must specify initialRouteName so the stack knows what to mount when the tab is tapped. Without it, the user lands on an empty route and the screen looks frozen even though the navigation event fired correctly. You can confirm this by adding a console.log in the screen's component — if the log fires but nothing renders, you're chasing a stack mounting issue, not a tab switching issue. The full debugging path is covered in Fixing expo-router screen navigation errors, which is worth bookmarking.
A third, rarer cause: a parent gesture handler swallowing the tap. If you've wrapped your root layout in GestureHandlerRootView with custom pan responders, a misconfigured handler can intercept tab presses. Temporarily remove the gesture handler to confirm; if tabs start working, you've found the culprit.
Pattern C: Production-only disappearing icons
Everything works in dev, but TestFlight or release APKs ship with blank tab icons. This is almost always a Metro tree-shaking interaction with the icon library. Packages like lucide-react-native rely on dynamic imports under the hood, and aggressive production bundling can drop those references because the bundler doesn't realize they're being used at runtime.
Try the following in order, since each fix is increasingly invasive:
- Open
metro.config.jsand check whether you've enabledresolver.unstable_enableSymlinks. Setting it to true frequently breaks module resolution in production; revert it to the default and rebuild. - Temporarily disable
experiments.tsconfigPathsinapp.jsonand rebuild. Path aliases combined with tree-shaking sometimes confuse the resolver into dropping icon entries. - Confirm
react-native-svgis on a stable version. Pre-release builds occasionally regress the production rendering path. - If none of the above helps, swap to a more battle-tested icon set such as
@expo/vector-iconsorreact-native-vector-icons. Switching libraries forces a different bundling path and almost always works.
I keep @expo/vector-icons as a fallback because it ships with the Expo SDK and almost never breaks under production bundling. Switching libraries means you're loading a different package entirely, which sidesteps the tree-shaking edge case. The visual difference is small enough that most users won't notice; the difference in stability is huge.
If you must keep lucide-react-native, one workaround is to import the icons explicitly at the top of your _layout.tsx instead of pulling them through a barrel file. Explicit imports tell the bundler exactly what to keep, even when tree-shaking is aggressive. It's verbose, but reliable.
Habits that prevent this bug entirely
A few practices that keep me out of this rabbit hole:
- Don't assume "missing icon = misconfiguration." Roughly half the time, the real cause is a missing peer dependency like
react-native-svg, or a Hermes-related quirk in production. Runexpo doctorfirst; it catches more than people give it credit for. - Match
Tabs.Screennameto file name 1:1. When asking Rork's AI to "add a tab," scan the new file tree before saving — the generator occasionally creates a name that doesn't exist as a route. A 5-second sanity check saves a 30-minute debugging session. - Always smoke-test the production build on TestFlight. Companion working ≠ production working. Add "verify tab bar visuals" as a one-line item in your release checklist; it's the cheapest insurance you can buy. The first time a missing icon ships to real users, you'll wish you had this habit.
- Pin your icon library version. Don't use a
^range forlucide-react-nativeor any peer-dependent icon package. A minor version bump that ships under^can quietly break production while leaving dev untouched.
If your symptom looks more like a state-update issue than a routing issue, Fixing state that won't update or trigger a rerender covers the adjacent failure modes worth ruling out before you start tearing apart your tab layout.
One thing to do after you've fixed it
After the bug is gone, extract app/(tabs)/_layout.tsx into its own component and leave a three-line comment at the top: "When adding a tab, install react-native-svg, match name= to the filename, and verify the production build via TestFlight." Future you (or a collaborator) will hit this same bug — that tiny checklist has saved me half a day more times than I'd like to admit.
The pattern of triaging-then-fixing also generalizes well. Most of the gnarly bugs in mobile development have two or three distinct failure modes wearing the same surface symptom. Spend the first minute classifying, and the next ten minutes are usually enough to fix it.