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
| Variant | Description |
|---|---|
small | Compact single-row bar (default) |
center-aligned | Title centered in the bar |
medium | Expanded two-row bar |
large | Expanded 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:
colorScheme | leading | trailing |
|---|---|---|
| The six surface schemes | onSurface | onSurfaceVariant |
primary | onPrimary | onPrimary |
primaryContainer | onPrimaryContainer | onPrimaryContainer |
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:
| Variant | Subtitle role | Bar height |
|---|---|---|
small, center-aligned | labelMedium | 64dp, no change |
medium | labelLarge | 112dp → 136dp |
large | titleMedium | 152dp → 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:
| Content | Distance from the bar edge |
|---|---|
| Title, with no leading content | titleStartInset + horizontalPadding = 16dp |
| Title, after a 48dp navigation icon | 4 + 48 + 4 = 56dp |
| End of the last action | horizontalPadding = 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
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
title | string | - | Yes | Title text displayed in the bar. |
subtitle | string | - | No | One 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. |
variant | enum | small | No | Layout variant. |
colorScheme | enum | surface | No | Color 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'`. |
canGoBack | boolean | | No | When `true`, renders the navigation button in the leading slot. Its icon is set by `navigationIcon`. |
navigationIcon | enum | back | No | Icon of the navigation button that `canGoBack` renders. Use `'close'` for a modal. The accessibility label follows the icon: "Go back" or "Close". |
onBackPress | () => void | - | No | Called when the auto-rendered navigation button is pressed. |
insetTop | boolean | | No | When `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. |
elevated | boolean | | No | When `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. |
scrollOffset | SharedValue<number> | - | No | Vertical 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. |
leading | ReactNode | - | No | Custom leading content. When provided, overrides `canGoBack` and `navigationIcon`. A standard `IconButton` in the slot takes the title color; other content reads it with `useAppBarContentColor()`. |
containerColor | string | - | No | Override the container (background) color. Applied to both normal and elevated states. |
contentColor | string | - | No | Override 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`. |
titleStyle | StyleProp<TextStyle> | - | No | Additional style applied to the title text. |
subtitleStyle | StyleProp<TextStyle> | - | No | Additional style applied to the subtitle text. |
titleLevel | enum | 1 | No | The 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. |
style | StyleProp<ViewStyle> | - | No | Custom style applied to the root container. |
hrefAttrs | { target?: string; rel?: string; download?: string | boolean; } | - | No | Web 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 | - |
actions | AppBarAction[] | - | No | Array of actions rendered in the trailing slot. Each entry is either an icon action (`{ icon }`) or a text action (`{ label }`, e.g. "Save"). |
trailing | ReactNode | - | No | Custom 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()`. |