Skip to main content

Native Tabs

Native tabs provide platform-native bottom tab navigation using iOS UITabBar and Android Material BottomNavigation components via react-native-screens.
Native tabs are currently in preview. The API may change. Use expo-router/unstable-native-tabs.

Installation

Ensure you have the required dependencies:
npx expo install react-native-screens

Basic Native Tabs

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

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home" />
      <NativeTabs.Trigger name="explore" />
      <NativeTabs.Trigger name="profile" />
    </NativeTabs>
  );
}
From the source (NativeTabs.tsx:9-36):
/**
 * The component used to create native tabs layout.
 *
 * @example
 * import { NativeTabs } from 'expo-router/unstable-native-tabs';
 *
 * export default function Layout() {
 *   return (
 *     <NativeTabs>
 *       <NativeTabs.Trigger name="home" />
 *       <NativeTabs.Trigger name="settings" />
 *     </NativeTabs>
 *   );
 * }
 */
export const NativeTabs = Object.assign(
  (props: NativeTabsProps) => {
    return <NativeTabsNavigatorWrapper {...props} />;
  },
  { Trigger: NativeTabTrigger, BottomAccessory }
);
From CLAUDE.md:241-253:
### Native Tabs

Native tabs provide native bottom tab navigation using iOS UITabBar 
and Android Material BottomNavigation via `react-native-screens`.

- `NativeTabs` - Layout component using `useNavigationBuilder`
- `NativeTabs.Trigger` - Tab configuration (icon, label, badge)
- `NativeTabs.BottomAccessory` - iOS 26+ bottom accessory
- Icons: `sf` (SF Symbols), `drawable` (Android), `md` (Material), `src` (images)
- iOS-specific: blur effects, transparency, sidebar adaptable
- Android-specific: Material Design 3, dynamic colors, ripple effects

Configure Tabs

With Labels and Icons

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

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home">
        <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon platform="ios" name="house" />
        <NativeTabs.Trigger.Icon platform="android" name="home" />
      </NativeTabs.Trigger>
      
      <NativeTabs.Trigger name="explore">
        <NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon platform="ios" name="safari" />
        <NativeTabs.Trigger.Icon platform="android" name="explore" />
      </NativeTabs.Trigger>
      
      <NativeTabs.Trigger name="profile">
        <NativeTabs.Trigger.Label>Profile</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon platform="ios" name="person" />
        <NativeTabs.Trigger.Icon platform="android" name="person" />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Icon Types

SF Symbols (iOS)

<NativeTabs.Trigger.Icon
  platform="ios"
  type="sf"
  name="house.fill"
/>

Material Icons (Android)

<NativeTabs.Trigger.Icon
  platform="android"
  type="md"
  name="home"
/>

Android Drawables

<NativeTabs.Trigger.Icon
  platform="android"
  type="drawable"
  name="ic_home"
/>

Image Source

<NativeTabs.Trigger.Icon
  type="src"
  source={require('./home-icon.png')}
/>

Badges

Add notification badges:
<NativeTabs.Trigger name="notifications">
  <NativeTabs.Trigger.Label>Notifications</NativeTabs.Trigger.Label>
  <NativeTabs.Trigger.Icon platform="ios" name="bell" />
  <NativeTabs.Trigger.Badge>5</NativeTabs.Trigger.Badge>
</NativeTabs.Trigger>
With dynamic count:
export default function TabLayout() {
  const [unreadCount, setUnreadCount] = useState(0);
  
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="messages">
        <NativeTabs.Trigger.Label>Messages</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon platform="ios" name="message" />
        {unreadCount > 0 && (
          <NativeTabs.Trigger.Badge>{unreadCount}</NativeTabs.Trigger.Badge>
        )}
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

iOS-Specific Features

Blur Effects

<NativeTabs
  appearance={{
    ios: {
      style: 'defaultBlur', // or 'opaqueBlur', 'transparent'
    },
  }}
/>

Scroll Edge Appearance

<NativeTabs
  appearance={{
    ios: {
      scrollEdgeAppearance: {
        style: 'opaqueBlur',
      },
    },
  }}
/>
<NativeTabs
  appearance={{
    ios: {
      sidebarAdaptable: true,
    },
  }}
/>

Android-Specific Features

Material Design 3

<NativeTabs
  appearance={{
    android: {
      elevation: 8,
      rippleColor: '#f4511e',
    },
  }}
/>

Dynamic Colors

<NativeTabs
  appearance={{
    android: {
      useDynamicColors: true,
    },
  }}
/>

Indicator Style

<NativeTabs
  appearance={{
    android: {
      indicatorStyle: 'pill', // or 'line'
    },
  }}
/>

Bottom Accessory (iOS 26+)

Add accessory content below tabs:
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  const placement = NativeTabs.BottomAccessory.usePlacement();
  
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home" />
      <NativeTabs.Trigger name="profile" />
      
      <NativeTabs.BottomAccessory>
        <View style={{ padding: 16, backgroundColor: '#f0f0f0' }}>
          <Text>Accessory Content</Text>
          <Text>Placement: {placement}</Text>
        </View>
      </NativeTabs.BottomAccessory>
    </NativeTabs>
  );
}

Hide Tabs

Hide tabs on specific screens:
app/details.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function Details() {
  return (
    <>
      <NativeTabs.Screen options={{ tabBarHidden: true }} />
      <View>
        <Text>Details Screen</Text>
      </View>
    </>
  );
}

Nested Navigation

Combine with Stack:
app/
  (tabs)/
    _layout.tsx
    home/
      _layout.tsx     # Stack
      index.tsx
      details.tsx
app/(tabs)/home/_layout.tsx
import { Stack } from 'expo-router';

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

Platform-Specific Tabs

Show different tabs per platform:
import { Platform } from 'react-native';
import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home">
        <NativeTabs.Trigger.Icon
          platform="ios"
          name="house.fill"
        />
        <NativeTabs.Trigger.Icon
          platform="android"
          name="home"
        />
      </NativeTabs.Trigger>
      
      {Platform.OS === 'ios' && (
        <NativeTabs.Trigger name="ios-only">
          <NativeTabs.Trigger.Label>iOS Only</NativeTabs.Trigger.Label>
        </NativeTabs.Trigger>
      )}
    </NativeTabs>
  );
}

Tab Configuration

Initial Tab

export const unstable_settings = {
  initialRouteName: 'home',
};

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home" />
      <NativeTabs.Trigger name="profile" />
    </NativeTabs>
  );
}

Tab Order

Tabs appear in the order they’re defined:
<NativeTabs>
  <NativeTabs.Trigger name="first" />   {/* Left/Top */}
  <NativeTabs.Trigger name="second" />
  <NativeTabs.Trigger name="third" />   {/* Right/Bottom */}
</NativeTabs>

Common Patterns

Conditional Tab Badge

export default function TabLayout() {
  const { hasUpdates } = useNotifications();
  
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="updates">
        <NativeTabs.Trigger.Label>Updates</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon platform="ios" name="bell" />
        {hasUpdates && (
          <NativeTabs.Trigger.Badge />
        )}
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Dynamic Icon

export default function TabLayout() {
  const { isOnline } = useConnection();
  
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="status">
        <NativeTabs.Trigger.Label>Status</NativeTabs.Trigger.Label>
        <NativeTabs.Trigger.Icon
          platform="ios"
          name={isOnline ? 'wifi' : 'wifi.slash'}
        />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Theme-Based Appearance

export default function TabLayout() {
  const { theme } = useTheme();
  
  return (
    <NativeTabs
      appearance={{
        ios: {
          style: theme === 'dark' ? 'opaqueBlur' : 'defaultBlur',
        },
        android: {
          useDynamicColors: theme === 'auto',
        },
      }}
    >
      <NativeTabs.Trigger name="home" />
    </NativeTabs>
  );
}

Migration from Tabs

Before (Regular Tabs)

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="home"
        options={{
          tabBarIcon: ({ color }) => <Icon name="home" color={color} />,
        }}
      />
    </Tabs>
  );
}

After (Native Tabs)

import { NativeTabs } from 'expo-router/unstable-native-tabs';

export default function TabLayout() {
  return (
    <NativeTabs>
      <NativeTabs.Trigger name="home">
        <NativeTabs.Trigger.Icon platform="ios" name="house" />
        <NativeTabs.Trigger.Icon platform="android" name="home" />
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

Best Practices

Use Native Icons

// Good: Platform-native icons
<NativeTabs.Trigger.Icon platform="ios" type="sf" name="house" />
<NativeTabs.Trigger.Icon platform="android" type="md" name="home" />

// Avoid: Custom images when native icons exist
<NativeTabs.Trigger.Icon type="src" source={require('./home.png')} />

Provide Platform-Specific Icons

// Good: Optimized for each platform
<NativeTabs.Trigger name="home">
  <NativeTabs.Trigger.Icon platform="ios" name="house.fill" />
  <NativeTabs.Trigger.Icon platform="android" name="home" />
</NativeTabs.Trigger>

// Avoid: Single icon for all platforms
<NativeTabs.Trigger name="home">
  <NativeTabs.Trigger.Icon type="src" source={require('./icon.png')} />
</NativeTabs.Trigger>

Keep Tab Count Reasonable

// Good: 3-5 tabs
<NativeTabs>
  <NativeTabs.Trigger name="home" />
  <NativeTabs.Trigger name="search" />
  <NativeTabs.Trigger name="profile" />
</NativeTabs>

// Avoid: Too many tabs
<NativeTabs>
  {/* 6+ tabs */}
</NativeTabs>

Limitations

  • Currently in preview - API may change
  • Requires react-native-screens 4.0+
  • Some features require iOS 26+ or Android 12+
  • Bottom accessory only works on iOS 26+
  • Sidebar adaptable only on iPadOS 18+

Next Steps

Tab Navigation

Compare with regular tabs

Stack Navigation

Combine with stacks

Layouts

Understand layouts

Navigation

Navigate between tabs