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#
- Every tab in a
bottomTabslayout must setbottomTab.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. - Register your tab item component with
Navigation.registerComponent(same as any other screen). - Optionally set
bottomTabs.customRowon the layout to style the floating row chrome (margins, height, background, corner radius).
Basic example#
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.
| Prop | Type | Description |
|---|---|---|
componentId | string | Stable id for this tab item instance |
tabIndex | number | Zero-based index of the tab |
selected | boolean | Whether this tab is currently selected |
badge | string \| null | Mirrors 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#
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:
Events#
Taps on custom tab items use the same native selection pipeline as the built-in tab bar. Use the standard Navigation events listeners:
| Listener | Custom row |
|---|---|
registerBottomTabPressedListener | Yes β every tap (iOS and Android) |
registerBottomTabSelectedListener | Yes β when the selected tab changes |
registerBottomTabLongPressedListener | No |
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.
Options apply on setRoot, showModal, mergeOptions, and setDefaultOptions (same as other bottom-tabs options).
customRow fields#
| Option | Type | Default (when omitted) | Description |
|---|---|---|---|
height | number | Native tab content height (+18 on iOS 26+) | Content height of the row in points/dp, excluding safe-area inset |
backgroundColor | color | β | Solid background; overrides backgroundEffect |
backgroundEffect | 'glass' \| 'blur' \| 'none' | 'glass' on iOS 26+, 'blur' below | Row background style |
cornerRadius | number | 28 on iOS 26+, 0 below | Corner radius of the row background |
horizontalMargin | number | 16 on iOS 26+, 0 below | Inset from left/right screen edges |
bottomMargin | number | 0 | Gap between row bottom and safe-area bottom |
Total row height = height + safe-area bottom inset + bottomMargin (same formula on both platforms).
Platform behavior#
| Topic | iOS | Android |
|---|---|---|
| Custom tab cells | React component in custom floating row | Same |
| Native tab bar visuals | Hidden; blank UITabBarItems keep selection | Native bar hidden (alpha = 0); BottomTabs still drives selection |
backgroundEffect: 'glass' | UIGlassEffect (iOS 26+), blur fallback | Semi-opaque material chrome (no system glass API) |
backgroundEffect: 'blur' | UIBlurEffect systemChromeMaterial | Semi-opaque material chrome |
| Row shadow | From glass/blur material | Subtle elevation shadow (~3dp, low alpha) to approximate iOS lift |
| Options delivery | Native options parser | JS forwards customRow to RNNBottomTabsCustomRowModule before layout commands |
| Min tabs | 2 (UIKit) | 3 (AHBottomNavigation) |
iOS implementation notes#
- Custom row:
RNNBottomTabsCustomRowβ hosts cells, appliescustomRowoptions, handles layout and taps. - iOS 26+: defaults match the floating tab bar (glass, 28pt radius, 16pt horizontal margin).
tabBarMinimizeBehavioris forced toneverwhen custom item views are active.
Android implementation notes#
- Custom row lives in
com.reactnativenavigation.customrowand attaches when aBottomTabsview 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 viaNavigationPackage.
Updating customRow at runtime#
On Android, merged customRow is picked up on the next layout pass via BottomTabsCustomRowConfigStore.