Skip to main content

TabBar

iOS Android
TabBar on iOS TabBar on Android

The platform tab bar: UITabBar on iOS and the Material 3 navigation bar (BottomNavigationView) on Android. Each tab has an icon over a label and an optional badge. On iOS 26 the bar is the floating Liquid Glass bar with its glass selection; earlier iOS shows the classic bar, and Android the Material bar with the active indicator pill.

TabBar draws the bar and reports presses; it doesn't manage screens. Render the selected screen yourself, or pass the bar to your navigation library as a custom tab bar (see With React Navigation).

import { TabBar } from 'react-native-platform-components';

const [tab, setTab] = useState('home');

<TabBar
items={[
{ label: 'Home', value: 'home', icon: { ios: 'house', android: 'home' }, selectedIcon: { ios: 'house.fill' } },
{ label: 'Search', value: 'search', icon: { ios: 'magnifyingglass', android: 'search' } },
{ label: 'Inbox', value: 'inbox', icon: { ios: 'bell', android: 'notifications' }, badge: unread || undefined },
]}
selectedValue={tab}
onSelect={setTab}
onReselect={scrollToTop}
/>

Props​

PropTypeDescription
itemsTabBarItem[]The tabs, at most five. See TabBarItem
selectedValuestring | nullThe selected tab's value; null for none
haptics'selection' | 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' | 'none'Haptic played when the user presses a tab, a reselect included. See Haptics. Default: none
onSelect(value: string, index: number) => voidA tab was pressed
onReselect(value: string, index: number) => voidThe selected tab was pressed again, the usual "scroll to top" or "back to root" gesture
labelVisibility'auto' | 'labeled' | 'selected' | 'unlabeled'How labels show. See Labels. Default: 'auto'
activeTintColorColorValueIcon and label color of the selected tab
inactiveTintColorColorValueIcon and label color of the other tabs
barColorColorValueBar background. See Styling
badgeStyle{ backgroundColor?, color? }Badge colors
labelStyle{ fontFamily?, fontSize?, fontWeight?, fontStyle? }Label font
minimizeBehavior'automatic' | 'never' | 'onScrollDown' | 'onScrollUp'Gets the bar out of the way as the content scrolls. See Minimize on scroll
scrollViewNativeIDstringThe nativeID of the ScrollView or FlatList that drives minimizeBehavior
accessoryReactNodeA view carried with the bar, such as a mini player. See Bottom accessory
onAccessoryEnvironmentChange(environment: 'regular' | 'inline') => voidiOS 26: the accessory moved into its row above the bar ('regular') or beside the minimized bar ('inline')
maxFontSizeMultipliernumberCap on the label font scale, as on Text. Android only; iOS tab labels have a fixed size
testIDstringTest identifier of the bar

TabBarItem​

PropTypeDescription
labelstringTab label
valuestringUnique value returned in callbacks
iconPlatformIconTab icon. See Icons
selectedIconPlatformIconIcon of the selected tab, such as the filled SF Symbol (house.fill). Defaults to icon
badgestring | numberBadge text or count; '' shows a dot; undefined hides it
disabledbooleanThe tab can't be selected
accessibilityLabelstringScreen-reader label. Defaults to label; the badge is announced after it
testIDstringTest identifier of the tab. See Testing
role'search'The search tab. See Search tab and system items
systemItemTabBarSystemItemiOS: a system tab item with the system's title and icon. See Search tab and system items

Android Props (android)​

PropTypeDescription
indicatorColorColorValueThe active indicator pill behind the selected icon
rippleColorColorValueRipple shown while pressing a tab
indicatorbooleanWhether the selected tab shows the active indicator. Default: true
indicatorShape'pill' | 'circle' | numberThe indicator's shape; a number is a corner radius in dp. Default: 'pill'
indicatorWidthnumberIndicator width in dp. Default: the Material width (64)
indicatorHeightnumberIndicator height in dp. Default: the Material height (32)
itemLayout'vertical' | 'horizontal' | 'auto'Icon above or beside the label. Default: 'vertical'. See Active indicator and horizontal tabs

Selection​

The bar is controlled: it shows the tab whose value is selectedValue. A press calls onSelect and the bar keeps its selection until selectedValue changes, so a press you ignore (a tab that needs sign-in first, say) leaves the selection where it was. Pressing the selected tab calls onReselect instead.

Labels​

labelVisibilityiOSAndroid
'auto' (default)Every tab labeledEvery tab labeled up to three tabs, the selected one from four
'labeled'Every tab labeledEvery tab labeled
'selected'Every tab labeledOnly the selected tab
'unlabeled'Icons onlyIcons only

Screen readers announce the label in every mode.

Badges​

badge takes a count or short text, shown in the platform's badge: red on iOS, the Material error color on Android. An empty string is a dot, for "something new" without a count. badgeStyle recolors them.

{ label: 'Inbox', value: 'inbox', icon: 'bell', badge: unread > 0 ? unread : undefined }
{ label: 'Updates', value: 'updates', icon: 'sparkles', badge: '' } // a dot

Search tab and system items​

role: 'search' makes a tab the app's search tab. On iOS it is the system search item, with the magnifying glass and the localized "Search" title; iOS 26 sets it apart from the other tabs as its own glass circle at the end of the bar, wherever it is in items. Android has no search tab: it's a regular tab with a search icon (your icon if you set one). Selecting it goes through onSelect like any other tab, and it's up to you what it shows.

iOS 26iOS 18Android
The search tab as its own circle on iOS 26The search tab on iOS 18The search tab on Android
items={[
{ label: 'Home', value: 'home', icon: { ios: 'house', android: 'home' } },
{ label: 'Inbox', value: 'inbox', icon: { ios: 'bell', android: 'notifications' } },
{ label: 'Search', value: 'search', role: 'search' },
]}

systemItem gives an iOS tab one of UIKit's system items (UITabBarItem.SystemItem), with the system's localized title and icon: 'bookmarks', 'contacts', 'downloads', 'favorites', 'featured', 'history', 'more', 'mostRecent', 'mostViewed', 'recents', 'search' or 'topRated'. The system decides both, so label and icon only apply on Android (and web), where there are no system items. systemItem: 'search' is the same tab as role: 'search' on iOS.

{ label: 'Favorites', value: 'favorites', systemItem: 'favorites', icon: { android: 'star' } }

A system item's title follows your app's localizations, as the system apps' do. For a search tab with your own title, use a regular tab with a magnifying glass icon instead.

Styling​

The bar takes the tint color on iOS and the Material 3 colors on Android, including a brand color set with useNativeTheme. activeTintColor and inactiveTintColor override the tab colors, android.indicatorColor the pill.

barColor replaces the bar background: the system chrome on iOS before 26, the Material surface container on Android. 'transparent' puts the bar on your own background. On iOS 26 the bar is always its floating Liquid Glass capsule; barColor doesn't replace the glass.

Active indicator and horizontal tabs (Android)​

The Material 3 bar marks the selected tab with the active indicator, a pill behind its icon. android.indicator: false turns it off, leaving the selected tab to its color. android.indicatorShape reshapes it: 'pill' (the default, fully rounded ends), 'circle' (a circle the indicator's height across) or a number, the corner radius in dp of a rounded rectangle. indicatorWidth and indicatorHeight size it in dp; the bar keeps its 80dp height, so an indicator taller than the default 32dp crowds the label.

android.itemLayout: 'horizontal' puts each icon beside its label, the Material 3 Expressive layout for wide bars; its indicator wraps the icon and label. 'auto' picks horizontal when the bar is at least 600dp wide (a tablet, or a phone in landscape) and vertical below that.

No indicatorCircle indicatorHorizontal tabs ('auto', landscape)
The bar without the active indicatorA circle active indicatorHorizontal tabs on a wide bar
<TabBar
items={items}
selectedValue={tab}
onSelect={setTab}
android={{ indicatorShape: 'circle', itemLayout: 'auto' }}
/>

iOS has no equivalent settings: its selection look is the system's.

Floating tabs​

On iOS 26 the tab bar floats by itself: a Liquid Glass capsule over your content, with room around it. On earlier iOS and on Android, put a transparent bar in a FloatingToolbar for the same layout:

import { FloatingToolbar, TabBar, isLiquidGlassSupported } from 'react-native-platform-components';

const bar = <TabBar items={items} selectedValue={tab} onSelect={setTab} barColor="transparent" style={{ flex: 1 }} />;

{isLiquidGlassSupported ? bar : <FloatingToolbar style={{ alignSelf: 'stretch' }}>{bar}</FloatingToolbar>}

Minimize on scroll​

minimizeBehavior gets the bar out of the way while the content scrolls, following the ScrollView (or FlatList) whose nativeID you pass as scrollViewNativeID. There's no scroll handler to write: the platform tracks the scrolling.

  • iOS 26: the system tab bar minimization. The bar shrinks into a small glass capsule showing the selected tab, and expands again as the content scrolls back. This is UITabBarController.tabBarMinimizeBehavior: with a minimizeBehavior, TabBar hosts its bar in a tab bar controller and hands it your ScrollView as the content scroll view.
  • Android: the Material behavior for a bottom bar, the bar sliding off the bottom edge and back (HideBottomViewOnScrollBehavior's motion). The bar always shows at the top of the content.
  • iOS before 26: the bar stays; the system has no minimized tab bar.
ValueBehavior
'onScrollDown'Minimize (Android: hide) while scrolling down, restore scrolling up
'onScrollUp'The reverse
'automatic'The platform default: iOS decides; Android hides on scroll down
'never'Always full size
<View style={{ flex: 1 }}>
<FlatList nativeID="feed" data={posts} renderItem={renderPost} />
<View style={{ position: 'absolute', left: 0, right: 0, bottom: insets.bottom }}>
<TabBar
items={items}
selectedValue={tab}
onSelect={setTab}
minimizeBehavior="onScrollDown"
scrollViewNativeID="feed"
/>
</View>
</View>

The Android bar slides down by its own height; place it at the bottom edge, or inside a view with overflow: 'hidden', so it leaves the screen rather than covering content below it. A ScrollView nested in another vertical ScrollView needs nestedScrollEnabled on Android to scroll at all.

Bottom accessory​

accessory puts a view of yours on the bar, such as the mini player of a music app.

  • iOS 26: the tab bar's bottom accessory (UITabBarController.bottomAccessory, a UITabAccessory). The system draws it as a glass row above the bar, and while the bar is minimized (see Minimize on scroll) it moves inline, beside the minimized tab. Your view is laid out at the accessory's size and follows it as it changes; onAccessoryEnvironmentChange reports the move ('inline', then 'regular' again), so you can switch to a compact layout.
  • Android and iOS before 26: a plain view above the bar. There's no system accessory, so your view draws its own background. On Android it slides away together with the bar under minimizeBehavior.

The bar's height includes the accessory's row, so place it as you would the bar alone. The accessory is a regular React view: presses, testIDs and state work as anywhere else.

iOS 26iOS 26, minimizediOS 18Android
The accessory above the bar on iOS 26The accessory inline beside the minimized barThe accessory as a view above the bar on iOS 18The accessory as a view above the bar on Android
const [environment, setEnvironment] = useState<TabBarAccessoryEnvironment>('regular');

<TabBar
items={items}
selectedValue={tab}
onSelect={setTab}
minimizeBehavior="onScrollDown"
scrollViewNativeID="library"
accessory={
<MiniPlayer
compact={environment === 'inline'}
style={isLiquidGlassSupported ? undefined : styles.playerCard}
/>
}
onAccessoryEnvironmentChange={setEnvironment}
/>

On iOS 26 the accessory content is hosted by UIKit and your view sits inside the system's glass capsule, so give it no background of its own there (isLiquidGlassSupported tells the two apart). The row is 48pt tall; lay the content out with flexbox rather than fixed widths, since the inline accessory is narrower.

Placement and safe areas​

The bar is a view in your layout: it fills the width it's given and takes the height the platform wants for it (49pt on iOS before 26, 83pt with the iOS 26 floating spacing, 80dp on Android), plus the accessory's row when it has one. It doesn't pad itself for the home indicator or the Android navigation bar; at the bottom of the screen, add the bottom inset around it, for example with react-native-safe-area-context:

const insets = useSafeAreaInsets();

<View style={{ paddingBottom: insets.bottom }}>
<TabBar items={items} selectedValue={tab} onSelect={setTab} />
</View>

Phones show at most five tabs; TabBar keeps the first five and warns in development.

With React Navigation​

A bottom-tab navigator takes a custom tab bar through its tabBar option:

<Tab.Navigator
tabBar={({ state, navigation, descriptors }) => (
<TabBar
items={state.routes.map((route) => ({
label: descriptors[route.key].options.title ?? route.name,
value: route.key,
icon: ICONS[route.name],
}))}
selectedValue={state.routes[state.index].key}
onSelect={(key) => {
const route = state.routes.find((r) => r.key === key)!;
navigation.navigate(route.name);
}}
onReselect={(key) => navigation.emit({ type: 'tabPress', target: key, canPreventDefault: true })}
/>
)}
>

Testing​

A tab's testID goes on the tab itself: the tab button on iOS (the circle, for the iOS 26 search tab), the navigation bar item on Android. E2E tests tap a tab by id:

await element(by.id('tab-inbox')).tap();

Android theme​

The bar is a Material 3 widget, so it works with a Theme.Material3 app theme and with the library's Material 3 fallback; see Android Theme Configuration.