Files
stack/packages/mosaic/framework/skills/vercel-react-native-skills/rules/navigation-native-navigators.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

4.8 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Native Navigators for Navigation HIGH native performance, platform-appropriate UI navigation, react-navigation, expo-router, native-stack, tabs

Use Native Navigators for Navigation

Always use native navigators instead of JS-based ones. Native navigators use platform APIs (UINavigationController on iOS, Fragment on Android) for better performance and native behavior.

For stacks: Use @react-navigation/native-stack or expo-router's default stack (which uses native-stack). Avoid @react-navigation/stack.

For tabs: Use react-native-bottom-tabs (native) or expo-router's native tabs. Avoid @react-navigation/bottom-tabs when native feel matters.

Stack Navigation

Incorrect (JS stack navigator):

import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

Correct (native stack with react-navigation):

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

Correct (expo-router uses native stack by default):

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function Layout() {
  return <Stack />;
}

Tab Navigation

Incorrect (JS bottom tabs):

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

function App() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

Correct (native bottom tabs with react-navigation):

import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';

const Tab = createNativeBottomTabNavigator();

function App() {
  return (
    <Tab.Navigator>
      <Tab.Screen
        name="Home"
        component={HomeScreen}
        options={{
          tabBarIcon: () => ({ sfSymbol: 'house' }),
        }}
      />
      <Tab.Screen
        name="Settings"
        component={SettingsScreen}
        options={{
          tabBarIcon: () => ({ sfSymbol: 'gear' }),
        }}
      />
    </Tab.Navigator>
  );
}

Correct (expo-router native tabs):

// app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="index">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon sf="house.fill" md="home" />
      </NativeTabs.Trigger>
      <NativeTabs.Trigger name="settings">
        <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon sf="gear" md="settings" />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

On iOS, native tabs automatically enable contentInsetAdjustmentBehavior on the first ScrollView at the root of each tab screen, so content scrolls correctly behind the translucent tab bar. If you need to disable this, use disableAutomaticContentInsets on the trigger.

Prefer Native Header Options Over Custom Components

Incorrect (custom header component):

<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  options={{
    header: () => <CustomHeader title="Profile" />,
  }}
/>

Correct (native header options):

<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  options={{
    title: 'Profile',
    headerLargeTitleEnabled: true,
    headerSearchBarOptions: {
      placeholder: 'Search',
    },
  }}
/>

Native headers support iOS large titles, search bars, blur effects, and proper safe area handling automatically.

Why Native Navigators

  • Performance: Native transitions and gestures run on the UI thread
  • Platform behavior: Automatic iOS large titles, Android material design
  • System integration: Scroll-to-top on tab tap, PiP avoidance, proper safe areas
  • Accessibility: Platform accessibility features work automatically

Reference: