Skip to main content
Version: Next

Custom bottom tab items

Custom bottom tabs let you render a React component for each tab item instead of the built-in icon + label. Use this when you need Lottie animations, SVG icons, custom badge layouts, or a fully custom tab bar design.

The same JavaScript API works on iOS and Android. Both platforms use a floating custom row when every tab declares bottomTab.component.

Requirements#

  1. Every tab in a bottomTabs layout must set bottomTab.component. If only some tabs use a custom component, RNN logs a warning and falls back to native rendering for all tabs in that bar.
  2. Register your tab item component with Navigation.registerComponent (same as any other screen).
  3. Optionally set bottomTabs.customRow on the layout to style the floating row chrome (margins, height, background, corner radius).

Basic example#

// 1. Register the tab item component (e.g. in index.js)
Navigation.registerComponent('MyTabItem', () => require('./MyTabItem'));
// 2. Use it on every tab
Navigation.setRoot({
root: {
bottomTabs: {
options: {
bottomTabs: {
customRow: {
backgroundEffect: 'glass',
cornerRadius: 28,
horizontalMargin: 16,
bottomMargin: 0,
height: 67,
},
},
},
children: [
{
stack: {
children: [{ component: { name: 'HomeScreen' } }],
options: {
bottomTab: {
component: { name: 'MyTabItem' },
badge: '3',
},
},
},
},
{
stack: {
children: [{ component: { name: 'SearchScreen' } }],
options: {
bottomTab: {
component: { name: 'MyTabItem' },
},
},
},
},
{
stack: {
children: [{ component: { name: 'ProfileScreen' } }],
options: {
bottomTab: {
component: { name: 'MyTabItem' },
},
},
},
},
],
},
},
});

See the playground: Layouts β†’ BottomTabs Custom Component (LayoutsScreen.bottomTabsWithCustomComponent).

Tab item component props#

RNN creates one React surface per tab and pushes prop updates when selection or badge changes.

PropTypeDescription
componentIdstringStable id for this tab item instance
tabIndexnumberZero-based index of the tab
selectedbooleanWhether this tab is currently selected
badgestring \| nullMirrors bottomTab.badge

passProps from bottomTab.component.passProps are merged at creation time only. Use tabIndex inside your component to pick icon/label per tab.

Example tab item#

function MyTabItem({ tabIndex, selected, badge }) {
const labels = ['Home', 'Search', 'Profile'];
return (
<View style={{ alignItems: 'center' }}>
<MyIcon index={tabIndex} selected={selected} />
<Text style={{ color: selected ? '#007aff' : '#9aa0a6' }}>
{labels[tabIndex]}
</Text>
{badge ? <Badge>{badge}</Badge> : null}
</View>
);
}

What is ignored when component is set#

For tabs that use bottomTab.component, these options are ignored for that tab's visual content:

text, icon, selectedIcon, sfSymbol, sfSelectedSymbol, iconColor, selectedIconColor, iconWidth, iconHeight, iconInsets, fontFamily, fontWeight, fontSize, selectedFontSize, textColor, selectedTextColor

Native still handles tab selection, visibility, drawBehind, animations, and dotIndicator. Switch tabs from JS with:

Navigation.mergeOptions('BOTTOM_TABS_LAYOUT_ID', {
bottomTabs: { currentTabIndex: 1 },
});

Events#

Taps on custom tab items use the same native selection pipeline as the built-in tab bar. Use the standard Navigation events listeners:

ListenerCustom row
registerBottomTabPressedListenerYes β€” every tap (iOS and Android)
registerBottomTabSelectedListenerYes β€” when the selected tab changes
registerBottomTabLongPressedListenerNo

bottomTab.selectTabOnPress: when false, pressed still fires; selected does not until you change the tab programmatically (for example via bottomTabs.currentTabIndex).

bottomTab.popToRoot and Android hardware-back tab history behave the same as with native tab items.

registerBottomTabLongPressedListener is iOS-only and is wired to the native UITabBar long-press gesture. With custom tabs the native bar is hidden and touches go to the custom row, so this listener does not fire for custom tab items.

Custom row (bottomTabs.customRow)#

When all tabs use custom components, RNN replaces the visible tab bar chrome with a floating row that hosts your React tab cells. Configure it with bottomTabs.customRow on the bottomTabs layout options.

bottomTabs: {
options: {
bottomTabs: {
customRow: {
height: 67,
backgroundEffect: 'glass', // 'glass' | 'blur' | 'none'
cornerRadius: 28,
horizontalMargin: 16,
bottomMargin: 0,
// backgroundColor: '#ffffff', // optional; overrides backgroundEffect
},
},
},
children: [/* ... */],
}

Options apply on setRoot, showModal, mergeOptions, and setDefaultOptions (same as other bottom-tabs options).

customRow fields#

OptionTypeDefault (when omitted)Description
heightnumberNative tab content height (+18 on iOS 26+)Content height of the row in points/dp, excluding safe-area inset
backgroundColorcolorβ€”Solid background; overrides backgroundEffect
backgroundEffect'glass' \| 'blur' \| 'none''glass' on iOS 26+, 'blur' belowRow background style
cornerRadiusnumber28 on iOS 26+, 0 belowCorner radius of the row background
horizontalMarginnumber16 on iOS 26+, 0 belowInset from left/right screen edges
bottomMarginnumber0Gap between row bottom and safe-area bottom

Total row height = height + safe-area bottom inset + bottomMargin (same formula on both platforms).

Platform behavior#

TopiciOSAndroid
Custom tab cellsReact component in custom floating rowSame
Native tab bar visualsHidden; blank UITabBarItems keep selectionNative bar hidden (alpha = 0); BottomTabs still drives selection
backgroundEffect: 'glass'UIGlassEffect (iOS 26+), blur fallbackSemi-opaque material chrome (no system glass API)
backgroundEffect: 'blur'UIBlurEffect systemChromeMaterialSemi-opaque material chrome
Row shadowFrom glass/blur materialSubtle elevation shadow (~3dp, low alpha) to approximate iOS lift
Options deliveryNative options parserJS forwards customRow to RNNBottomTabsCustomRowModule before layout commands
Min tabs2 (UIKit)3 (AHBottomNavigation)

iOS implementation notes#

  • Custom row: RNNBottomTabsCustomRow β€” hosts cells, applies customRow options, handles layout and taps.
  • iOS 26+: defaults match the floating tab bar (glass, 28pt radius, 16pt horizontal margin).
  • tabBarMinimizeBehavior is forced to never when custom item views are active.

Android implementation notes#

  • Custom row lives in com.reactnativenavigation.customrow and attaches when a BottomTabs view with custom items appears.
  • BottomTabs.setExternalCustomItemViewHost(true) prevents the legacy overlay path from re-parenting React views into native cells.
  • No changes required in app MainApplication β€” the module registers via NavigationPackage.

Updating customRow at runtime#

Navigation.mergeOptions('BOTTOM_TABS_LAYOUT_ID', {
bottomTabs: {
customRow: {
backgroundColor: '#1c1c1e',
cornerRadius: 20,
},
},
});

On Android, merged customRow is picked up on the next layout pass via BottomTabsCustomRowConfigStore.

Related API#