Skip to main content

AppBar

Top app bars display navigation, title, and actions at the top of the screen. Follows the Material Design 3 Top App Bar specification.

Usage​

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

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1 }}>
<AppBar title="Home" />
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Variants​

VariantDescription
smallCompact single-row bar (default)
center-alignedTitle centered in the bar
mediumExpanded two-row bar
largeExpanded with larger title typography

With Back Button​

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

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1 }}>
<AppBar
title="Details"
canGoBack
onBackPress={() => Alert.alert('Back pressed')}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

In a Modal​

A modal closes, so it shows a cross, not a back arrow. Set navigationIcon="close". The button keeps the built-in size, colour, and position, and it announces "Close" to screen readers. onBackPress fires for both icons.

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

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1 }}>
<AppBar
title="About"
canGoBack
navigationIcon="close"
onBackPress={() => Alert.alert('Close pressed')}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

For any other leading control, pass it to leading. The bar centres custom leading and trailing content on the title, so an IconButton needs no wrapper.

With Actions​

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

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1 }}>
<AppBar
title="Inbox"
actions={[
{ icon: 'magnify', accessibilityLabel: 'Search', onPress: () => Alert.alert('Search') },
{ icon: 'dots-vertical', accessibilityLabel: 'More', onPress: () => Alert.alert('More') },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Custom Slot Content​

leading and trailing take any content, for example a Menu anchor. Each slot gives its icon color to each standard IconButton in it, and the button's hover and press layers derive from that color. The colors come from the MD3 top app bar tokens:

colorSchemeleadingtrailing
The six surface schemesonSurfaceonSurfaceVariant
primaryonPrimaryonPrimary
primaryContaineronPrimaryContaineronPrimaryContainer

The bar's contentColor replaces both. The built-in navigation button and icon actions use the same colors. A selected toggle keeps primary, and a disabled button keeps the MD3 disabled color. Set iconColor on the button to use a different color.

For other content, such as an Icon or a text, read the slot color with useAppBarContentColor(). The hook returns undefined outside the two slots. Content that a Portal renders, such as the open menu, does not get the color.

import {
AppBar,
Icon,
IconButton,
Menu,
PortalHost,
useAppBarContentColor,
} from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { Alert, View } from 'react-native'

function SyncState() {
const color = useAppBarContentColor()
return <Icon source="cloud-check-outline" color={color} accessibilityLabel="Synced" />
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<PortalHost>
<View style={{ flex: 1 }}>
<AppBar
title="Notes"
colorScheme="primary"
trailing={
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<SyncState />
<Menu
align="end"
anchor={
<IconButton
icon="dots-vertical"
variant="standard"
accessibilityLabel="More options"
/>
}
>
<Menu.Item label="Settings" onPress={() => Alert.alert('Settings')} />
</Menu>
</View>
}
/>
</View>
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

With a Subtitle​

subtitle adds one line of supporting text under the title. Its color is onSurfaceVariant. Its type role follows the variant:

VariantSubtitle roleBar height
small, center-alignedlabelMedium64dp, no change
mediumlabelLarge112dp → 136dp
largetitleMedium152dp → 184dp

The roles and the growth come from the MD3 Expressive flexible app bars. The baseline medium and large bars have no subtitle in the spec, so they take the subtitle of the flexible bars and keep their own title.

contentColor does not change the subtitle color. Use subtitleStyle for that. On the primary and primaryContainer schemes, the subtitle takes the scheme's content color.

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

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, rowGap: 16 }}>
<AppBar
title="Inbox"
subtitle="3 unread"
actions={[{ icon: 'magnify', accessibilityLabel: 'Search' }]}
/>
<AppBar
title="Processes"
subtitle="The list updates every 3 seconds."
variant="medium"
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Collapse on Scroll​

The medium and large variants collapse to the small (64dp) form as the content scrolls, per the MD3 spec: the container height shrinks, the title transitions to titleLarge typography, and the on-scroll tonal shift is applied automatically — no elevated toggling needed.

Wire it up with useScroll from @rootnative/inertia: pass scrollY to the bar's scrollOffset prop and the matching onScroll to a Motion.ScrollView. Everything runs on the UI thread — there is no JS-side scroll listener.

import { AppBar, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { Motion, useScroll } from '@rootnative/inertia'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

function Screen() {
const { scrollY, onScroll } = useScroll()

return (
<View style={{ flex: 1 }}>
<AppBar title="Large App Bar" variant="large" scrollOffset={scrollY} />
<Motion.ScrollView
onScroll={onScroll}
scrollEventThrottle={16}
contentContainerStyle={{ padding: 16, rowGap: 16 }}
>
{Array.from({ length: 30 }, (_, i) => (
<Typography key={i}>Scroll content row {i + 1}</Typography>
))}
</Motion.ScrollView>
</View>
)
}

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

The bar collapses over a scroll distance equal to the height it loses (48px for medium, 88px for large), so the motion tracks the finger 1:1. With a subtitle, the bar is taller, so the distance is 72px for medium and 120px for large. The subtitle collapses to labelMedium with the title. scrollOffset is ignored by the small and center-aligned variants — keep using elevated for those.

Align with a Page Gutter​

The bar follows the Compose TopAppBarLayout geometry. Each side slot and the title carry horizontalPadding, so with the default theme.topAppBar tokens:

ContentDistance from the bar edge
Title, with no leading contenttitleStartInset + horizontalPadding = 16dp
Title, after a 48dp navigation icon4 + 48 + 4 = 56dp
End of the last actionhorizontalPadding = 4dp

horizontalPadding counts twice for the title and once for the actions, so no pair of token values puts both on one wider gutter. For a page with a wider gutter, keep the tokens and pad the bar with style. For a 32dp gutter:

import { AppBar } from '@rootnative/components/appbar'
import { defaultTopAppBarTokens } from '@rootnative/core'
import { StyleSheet } from 'react-native'

const GUTTER = 32
const { horizontalPadding, titleStartInset } = defaultTopAppBarTokens

const styles = StyleSheet.create({
bar: {
paddingStart: GUTTER - horizontalPadding - titleStartInset,
paddingEnd: GUTTER - horizontalPadding,
},
})

<AppBar title="Storage" subtitle="Find files that take space." style={styles.bar} />

The title then starts 32dp from the start edge, and the last action ends 32dp from the end edge. The TopAppBarTokens type documents each token.

Props​

PropTypeDefaultRequiredDescription
titlestring-YesTitle text displayed in the bar.
subtitlestring-NoOne line of supporting text under the title. Its type role follows the variant: `labelMedium` on `'small'` and `'center-aligned'`, `labelLarge` on `'medium'`, and `titleMedium` on `'large'`. Its color is `onSurfaceVariant`, or the scheme's content color on the `'primary'` and `'primaryContainer'` schemes. A `'small'` or `'center-aligned'` bar keeps its 64dp height. A `'medium'` bar grows by 24dp and a `'large'` bar by 32dp. A bar that collapses with `scrollOffset` takes the subtitle to `labelMedium` with the title.
variantenumsmallNoLayout variant.
colorSchemeenumsurfaceNoColor scheme that determines the default container and content colors. `containerColor` and `contentColor` props override these defaults. The trailing icons take `onSurfaceVariant` on the surface schemes, as the MD3 tokens give them, and the content color on `'primary'` and `'primaryContainer'`.
canGoBackbooleanNoWhen `true`, renders the navigation button in the leading slot. Its icon is set by `navigationIcon`.
navigationIconenumbackNoIcon of the navigation button that `canGoBack` renders. Use `'close'` for a modal. The accessibility label follows the icon: "Go back" or "Close".
onBackPress() => void-NoCalled when the auto-rendered navigation button is pressed.
insetTopbooleanNoWhen `true`, pads the bar by the top safe-area inset. Inside a `SafeAreaProvider` the inset comes from context, so it is correct on the first paint of a modal route. Without a provider the bar falls back to the native `SafeAreaView`, and on web it applies no inset.
elevatedbooleanNoWhen `true`, applies the MD3 on-scroll tonal shift: the container color animates from its resting color to its elevated color (`surface` → `surfaceContainer` for the default `'surface'` color scheme). No shadow is added. Only the `'surface'` scheme defines a distinct elevated color; other schemes keep the same container color. Toggle this from your scroll handler when content scrolls under the bar. When `scrollOffset` is provided on a `'medium'`/`'large'` bar, the tonal shift is driven from the scroll position automatically and this prop only forces the elevated color on.
scrollOffsetSharedValue<number>-NoVertical scroll offset (in px) of the content scrolling under the bar, as a Reanimated shared value. Pass `scrollY` from `@rootnative/inertia`'s `useScroll()` and give the matching `onScroll` to your scroll view: ```tsx // `Motion` and `useScroll` both come from the inertia package root. // There is no `@rootnative/inertia/motion` subpath. import { Motion, useScroll } from '@rootnative/inertia' const { scrollY, onScroll } = useScroll() <AppBar variant="large" title="Title" scrollOffset={scrollY} /> <Motion.ScrollView onScroll={onScroll} scrollEventThrottle={16}> ``` Drives the MD3 collapse-on-scroll behavior of the `'medium'` and `'large'` variants: the container collapses to the small (64dp) form and the title interpolates to `titleLarge` as the user scrolls, with the on-scroll tonal shift riding the same progress. Ignored by the `'small'` and `'center-aligned'` variants.
leadingReactNode-NoCustom leading content. When provided, overrides `canGoBack` and `navigationIcon`. A standard `IconButton` in the slot takes the title color; other content reads it with `useAppBarContentColor()`.
containerColorstring-NoOverride the container (background) color. Applied to both normal and elevated states.
contentColorstring-NoOverride the content (title and icon) color. It replaces the icon color of both slots, so it also reaches each standard `IconButton` in `leading` and `trailing`. The subtitle keeps its own color; set it with `subtitleStyle`.
titleStyleStyleProp<TextStyle>-NoAdditional style applied to the title text.
subtitleStyleStyleProp<TextStyle>-NoAdditional style applied to the subtitle text.
titleLevelenum1NoThe outline level of the title. The bar title is the page heading, so the web renders it as `<h1>`. Pass `2` when a heading above the bar is the page heading.
styleStyleProp<ViewStyle>-NoCustom style applied to the root container.
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-
actionsAppBarAction[]-NoArray of actions rendered in the trailing slot. Each entry is either an icon action (`{ icon }`) or a text action (`{ label }`, e.g. "Save").
trailingReactNode-NoCustom trailing content. Use this instead of `actions` when the slot needs a component `actions` cannot build, such as a `Menu` anchor or a `Tooltip`. A standard `IconButton` in the slot takes the trailing icon color (see `colorScheme`); other content reads it with `useAppBarContentColor()`.