# Native Tabs
Always prefer NativeTabs from 'expo-router/unstable-native-tabs' for the best iOS experience.
**SDK 54+. SDK 55 recommended.**
## SDK Compatibility
| Aspect | SDK 54 | SDK 55+ |
| ------------- | ------------------------------------------------------- | ----------------------------------------------------------- |
| Import | `import { NativeTabs, Icon, Label, Badge, VectorIcon }` | `import { NativeTabs }` only |
| Icon | `` | `` |
| Label | `` | `Home` |
| Badge | `9+` | `9+` |
| Android icons | `drawable` prop | `md` prop (Material Symbols) |
All examples below use SDK 55 syntax. For SDK 54, replace `NativeTabs.Trigger.Icon/Label/Badge` with standalone `Icon`, `Label`, `Badge` imports.
## Basic Usage
```tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
Home9+SettingsSearch
);
}
```
## Rules
- You must include a trigger for each tab
- The `NativeTabs.Trigger` 'name' must match the route name, including parentheses (e.g. ``)
- Prefer search tab to be last in the list so it can combine with the search bar
- Use the 'role' prop for common tab types
- Tabs must be static — no dynamic addition/removal at runtime (remounts navigator, loses state)
## Platform Features
Native Tabs use platform-specific tab bar implementations:
- **iOS 26+**: Liquid glass effects with system-native appearance
- **Android**: Material 3 bottom navigation
- Better performance and native feel
## Icon Component
```tsx
// SF Symbol (iOS) + Material Symbol (Android)
// State variants
// Custom image
// Xcode asset catalog — iOS only (SDK 55+)
// Rendering mode — iOS only (SDK 55+)
```
`renderingMode`: `"template"` applies tint color (single-color icons), `"original"` preserves source colors (gradients). Android always uses original.
## Label & Badge
```tsx
// Label
HomeHome {/* icon-only tab */}
// Badge
9+ {/* dot indicator */}
```
## iOS 26 Features
### Liquid Glass Tab Bar
The tab bar automatically adopts liquid glass appearance on iOS 26+.
### Minimize on Scroll
```tsx
```
### Search Tab
```tsx
Search
```
**Note**: Place search tab last for best UX.
### Role Prop
Use semantic roles for special tab types:
```tsx
```
Available roles: `search` | `more` | `favorites` | `bookmarks` | `contacts` | `downloads` | `featured` | `history` | `mostRecent` | `mostViewed` | `recents` | `topRated`
## Customization
### Tint Color
```tsx
```
### Dynamic Colors (iOS)
Use DynamicColorIOS for colors that adapt to liquid glass:
```tsx
import { DynamicColorIOS, Platform } from 'react-native';
const adaptiveBlue = Platform.select({
ios: DynamicColorIOS({ light: '#007AFF', dark: '#0A84FF' }),
default: '#007AFF',
});
```
## Conditional Tabs
```tsx
Admin
```
**Don't hide the tabs when they are visible - toggling visibility remounts the navigator; Do it only during the initial render.**
**Note**: Hidden tabs cannot be navigated to!
## Behavior Options
```tsx
```
## Hidden Tab Bar (SDK 55+)
Use `hidden` prop on `NativeTabs` to hide the entire tab bar dynamically:
```tsx
{/* triggers */}
```
## Bottom Accessory (SDK 55+)
`NativeTabs.BottomAccessory` renders content above the tab bar (iOS 26+). Uses `usePlacement()` to adapt between `'regular'` and `'inline'` layouts.
**Important**: Two instances render simultaneously — store state outside the component (props, context, or external store).
```tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
function MiniPlayer({
isPlaying,
onToggle,
}: {
isPlaying: boolean;
onToggle: () => void;
}) {
const placement = NativeTabs.BottomAccessory.usePlacement();
if (placement === "inline") {
return (
);
}
return {/* full player UI */};
}
export default function TabLayout() {
const [isPlaying, setIsPlaying] = useState(false);
return (
setIsPlaying(!isPlaying)}
/>
Home
);
}
```
## Safe Area Handling (SDK 55+)
SDK 55 handles safe areas automatically:
- **Android**: Content wrapped in SafeAreaView (bottom inset)
- **iOS**: First ScrollView gets automatic `contentInsetAdjustmentBehavior`
To opt out per-tab, use `disableAutomaticContentInsets` and manage manually:
```tsx
Home
```
```tsx
// In the screen
import { SafeAreaView } from "react-native-screens/experimental";
export default function HomeScreen() {
return (
{/* content */}
);
}
```
## Using Vector Icons
If you must use @expo/vector-icons instead of SF Symbols:
```tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
import Ionicons from "@expo/vector-icons/Ionicons";
Home
```
**Prefer SF Symbols + `md` prop over vector icons for native feel.**
If you are using SDK 55 and later **use the md prop to specify Material Symbols used on Android**.
## Structure with Stacks
Native tabs don't render headers. Nest Stacks inside each tab for navigation headers:
```tsx
// app/(tabs)/_layout.tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
Home
);
}
// app/(tabs)/(home)/_layout.tsx
import Stack from "expo-router/stack";
export default function HomeStack() {
return (
);
}
```
## Custom Web Layout
Use platform-specific files for separate native and web tab layouts:
```
app/
_layout.tsx # NativeTabs for iOS/Android
_layout.web.tsx # Headless tabs for web (expo-router/ui)
```
Or extract to a component: `components/app-tabs.tsx` + `components/app-tabs.web.tsx`.
## Migration from JS Tabs
### Before (JS Tabs)
```tsx
import { Tabs } from "expo-router";
,
tabBarBadge: 3,
}}
/>
;
```
### After (Native Tabs)
```tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
Home3;
```
### Key Differences
| JS Tabs | Native Tabs |
| -------------------------- | ---------------------------- |
| `` | `` |
| `options={{ title }}` | `` |
| `options={{ tabBarIcon }}` | `` |
| `tabBarBadge` option | `` |
| Props-based API | Component-based API |
| Headers built-in | Nest `` for headers |
## Limitations
- **Android**: Maximum 5 tabs (Material Design constraint)
- **Nesting**: Native tabs cannot nest inside other native tabs
- **Tab bar height**: Cannot be measured programmatically
- **FlatList transparency**: Use `disableTransparentOnScrollEdge` to fix issues
- **Dynamic tabs**: Tabs must be static; changes remount navigator and lose state
## Keyboard Handling (Android)
Configure in app.json:
```json
{
"expo": {
"android": {
"softwareKeyboardLayoutMode": "resize"
}
}
}
```
## Common Issues
1. **Icons not showing on Android**: Add `md` prop (SDK 55) or use VectorIcon
2. **Headers missing**: Nest a Stack inside each tab group
3. **Trigger name mismatch**: `name` must match exact route name including parentheses
4. **Badge not visible**: Badge must be a child of Trigger, not a prop
5. **Tab bar transparent on iOS 18 and earlier**: If the screen uses a `ScrollView` or `FlatList`, make sure it is the first opaque child of the screen component. If it needs to be wrapped in another `View`, ensure the wrapper uses `collapsable={false}`. If the screen does not use a `ScrollView` or `FlatList`, set `disableTransparentOnScrollEdge` to `true` in the `NativeTabs.Trigger` options, to make the tab bar opaque.
6. **Scroll to top not working**: Ensure `disableScrollToTop` is not set on the active tab's Trigger and `ScrollView` is the first child of the screen component.
7. **Header buttons flicker when navigating between tabs**: Make sure the app is wrapped in a `ThemeProvider`
```tsx
import {
ThemeProvider,
DarkTheme,
DefaultTheme,
} from "@react-navigation/native";
import { useColorScheme } from "react-native";
import { Stack } from "expo-router";
export default function Layout() {
const colorScheme = useColorScheme();
return (
);
}
```
If the app only uses a light or dark theme, you can directly pass `DarkTheme` or `DefaultTheme` to `ThemeProvider` without checking the color scheme.
```tsx
import { ThemeProvider, DarkTheme } from "@react-navigation/native";
import { Stack } from "expo-router";
export default function Layout() {
return (
);
}
```