Skip to main content

NavigationBar

Navigation bars let people switch between an app's top-level destinations on compact screens. Follows the Material Design 3 Navigation bar specification.

Each destination is an icon over a label, with the active one marked by a secondaryContainer pill behind its icon. The bar is 80dp tall on a surfaceContainer background.

NavigationBar is a bar, not a navigator

NavigationBar renders the destinations and tells you which one was pressed. It does not own screens, mount panels, or integrate with a navigator. Point it at whatever you already use — see Wiring it to a navigator.

Usage​

Pass the destinations as items and read the selection from onValueChange. With no value prop, the bar tracks the active destination itself and starts on the first item.

import { NavigationBar } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { Text, View } from 'react-native'

const DESTINATIONS = [
{ value: 'home', label: 'Home', icon: 'home-outline', selectedIcon: 'home' },
{ value: 'search', label: 'Search', icon: 'magnify' },
{ value: 'library', label: 'Library', icon: 'bookshelf' },
]

const PANELS = {
home: 'Fresh picks for you.',
search: 'Type to find anything.',
library: 'Everything you saved.',
}

function Screen() {
const [destination, setDestination] = useState('home')
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, padding: 24 }}>
<Text>{PANELS[destination]}</Text>
</View>
<NavigationBar
items={DESTINATIONS}
value={destination}
onValueChange={setDestination}
insetBottom
/>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<Screen />
</ThemeProvider>
</SafeAreaProvider>
)
}

MD3 recommends 3–5 destinations. The row is split equally between them, whatever the count.

An item's selectedIcon is shown while it is active — MD3 pairs an outlined resting icon with its filled counterpart. Leave it off to keep the same icon in both states.

Selection​

value + onValueChange is the controlled form; leave value off and the bar tracks the selection itself, starting on defaultValue or the first item. A navigation bar with nothing active reads as broken, which is why there is no "no selection" state. onValueChange fires on every press, controlled or not.

Wiring it to a navigator​

NavigationBar deliberately stops at the bar. With expo-router, drive it from the current route:

import { NavigationBar } from '@rootnative/components'
import { usePathname, useRouter } from 'expo-router'

const DESTINATIONS = [
{ value: '/home', label: 'Home', icon: 'home-outline', selectedIcon: 'home' },
{ value: '/search', label: 'Search', icon: 'magnify' },
]

function AppShellBar() {
const pathname = usePathname()
const router = useRouter()

return (
<NavigationBar
items={DESTINATIONS}
value={DESTINATIONS.find((d) => pathname.startsWith(d.value))?.value}
onValueChange={(value) => router.replace(value)}
insetBottom
/>
)
}

The same shape works for react-navigation — render it as a custom tabBar and map state.index to value.

Label visibility​

labelVisibility controls when destination labels are shown:

ValueMeaning
'always' (default)Every destination keeps its label
'selected'Only the active destination's label is visible — it fades in on selection while the inactive icons sit centred
'never'No labels; every icon is centred

The label stays required on the item either way: it is also the destination's screen-reader name.

The indicator​

The active indicator is a 56×32dp secondaryContainer pill behind the icon. On selection it fades in while expanding from its centre, riding springDefaultSpatial, and collapses to a hard cut under reduced motion — see Motion. Hover, press, and focus state layers paint inside the same pill shape, on active and inactive destinations alike.

Safe area​

A bottom-anchored bar must clear the home indicator. Pass insetBottom to wrap the row in a SafeAreaView that adds the bottom inset below the bar's 80dp — leave it off (the default) when an ancestor already applies the bottom edge, or the inset would be added twice.

Overrides​

PropTarget
containerColorThe bar background
contentColorIcon and label color of inactive destinations
selectedContentColorIcon and label color of the active destination; the state layers re-derive from it
indicatorColorThe active indicator pill
labelStyleEvery destination's label Text — does not affect the icons
styleThe bar container

Disabled destinations always use the MD3 disabled treatment (38% content); no override changes that.

When you pass testID, each destination carries <testID>-item-<value> and its indicator <testID>-item-<value>-indicator so a test can assert on them.

Tokens​

TokenValue
ContainersurfaceContainer, elevation level 2
Height80dp (+ bottom inset with insetBottom)
Active indicator56×32dp pill, secondaryContainer, full corners
Icon24dp — active onSecondaryContainer, inactive onSurfaceVariant
LabellabelMedium — active secondary, inactive onSurfaceVariant
Indicator → label gap4dp
Item padding8dp horizontal, destinations split the row equally

These are the Expressive-updated androidx tokens: the indicator is 56dp wide (not the pre-Expressive 64dp) and the active label is secondary (not onSurface). The 64dp-tall Expressive short bar is a possible 1.x variant; this bar uses compose's TallContainerHeight.

Accessibility​

  • The bar reports role="tablist" and each destination role="tab" with accessibilityState.selected. Pass accessibilityLabel to name the bar itself ("Main navigation").
  • A destination's screen-reader name falls back to its label — including when labelVisibility hides the visible label; set accessibilityLabel on the item to override it.
  • Disabled destinations expose accessibilityState.disabled and stop responding.
  • On web, destinations show a keyboard focus ring (secondary, 3dp) around the indicator pill, driven by the same focus-visible state as the rest of the library.

Props​

PropTypeDefaultRequiredDescription
itemsNavigationBarItem[]-YesThe destinations to render. MD3 recommends 3–5.
valuestring-NoValue of the active destination (controlled).
defaultValuestring-NoValue of the initially active destination (uncontrolled). Defaults to the first item — a navigation bar with nothing active reads as broken.
onValueChange(value: string) => void-NoCalled with the value of the destination that was pressed.
labelVisibilityenumalwaysNoWhen destination labels are shown.
insetBottombooleanNoWhen `true`, wraps the row in a SafeAreaView that adds the bottom inset below the bar's 80dp, so it clears the home indicator. Leave it off when an ancestor (e.g. `Layout` with a `bottom` edge) already handles it.
containerColorstringsurfaceContainerNoOverride the bar background.
contentColorstringonSurfaceVariantNoOverride the icon and label color of inactive destinations.
selectedContentColorstringonSecondaryContainer (icon) / secondary (label)NoOverride the icon and label color of the active destination. State-layer colors are derived from it automatically.
indicatorColorstringsecondaryContainerNoOverride the active indicator pill color.
labelStyleStyleProp<TextStyle>-NoStyle applied to every destination's label `Text` — does not affect the icons.
styleStyleProp<ViewStyle>-NoStyle applied to the bar container.
accessibilityLabelstring-NoScreen-reader label for the bar itself.
testIDstring-NoTest id applied to the bar container. Each destination carries `<testID>-item-<value>` and its indicator `<testID>-item-<value>-indicator`.
hrefAttrs{ target?: string; rel?: string; download?: string | boolean; }-NoWeb only. The `target`, `rel` and `download` of the `<a>` that a view with an `href` renders, for example under Expo Router's `<Link asChild>`. `target: '_blank'` opens the link in a new tab. Native ignores it.
onKeyDown(event: { nativeEvent: { key?: string; }; }) => void-No-