Expo UI and Router for iOS
Expo Router

Stack toolbar

Learn how to use the native toolbar in Stack navigation with Expo Router.

Stack.Toolbar is an alpha API available on Android in Expo SDK 56 and later, and on iOS in Expo SDK 55 and later. The API is subject to breaking changes.

Stack.Toolbar lets you add native toolbar items to your Stack screens on Android and iOS. You can place buttons, menus, and custom views in the header (left or right side) or in the bottom toolbar.

Adding header buttons

Use Stack.Toolbar.Button within Stack.Toolbar with placement="right" or placement="left" to add buttons to the navigation header. This is useful for actions like favoriting, sharing, or editing content.

iOS

import { useState } from 'react';
import { Stack } from 'expo-router';
import { View, Text, Alert } from 'react-native';

export default function NoteScreen() {
  const [isFavorite, setIsFavorite] = useState(false);

  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Button
          icon={isFavorite ? 'star.fill' : 'star'}
          onPress={() => setIsFavorite(!isFavorite)}
        />
        <Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => Alert.alert('Share')} />
      </Stack.Toolbar>
      <Stack.Toolbar placement="left">
        <Stack.Toolbar.Button icon="sidebar.left" onPress={() => Alert.alert('Sidebar')} />
      </Stack.Toolbar>

      <View style={{ flex: 1, padding: 16 }}>
        <Text>Note content...</Text>
      </View>
    </>
  );
}

Icons

Toolbar buttons accept SF Symbols (iOS only) and custom images (Android and iOS).

SF Symbols (iOS only)

The easiest way to add icons on iOS is to use SF Symbols, Apple's built-in icon library. Pass the symbol name directly to the icon prop:

<Stack.Toolbar.Button icon="star.fill" onPress={() => {}} />
<Stack.Toolbar.Button icon="square.and.arrow.up" onPress={() => {}} />
<Stack.Toolbar.Menu icon="ellipsis.circle">{/* ... */}</Stack.Toolbar.Menu>

You can browse available symbols in Apple's SF Symbols app.

SF Symbols are an iOS-only feature.

Custom images

You can also use custom images. The API for passing them differs by platform:

iOS

iOS uses two different APIs depending on placement: pass an image source directly to icon for header toolbars, and use useImage with the image prop for the bottom toolbar.

Using custom images inside submenus (Stack.Toolbar.Menu) in header placements requires react-native-screens 4.24.0 or later. SDK 55 bundles ~4.23.0, so you need to install react-native-screens@~4.24.0 manually to use this feature. SDK 56 bundles a compatible version by default.

import { Stack } from 'expo-router';

export default function Page() {
  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Button icon={require('./assets/expo.png')} onPress={() => {}} />
      </Stack.Toolbar>
      {/* Screen content */}
    </>
  );
}

In the bottom toolbar, use the useImage hook from expo-image and pass the result to the image prop:

import { Stack } from 'expo-router';
import { useImage } from 'expo-image';

export default function Page() {
  const customIcon = useImage('https://simpleicons.org/icons/expo.svg', {
    maxWidth: 24,
    maxHeight: 24,
  });

  return (
    <>
      <Stack.Toolbar>
        <Stack.Toolbar.Button image={customIcon} onPress={() => {}} />
      </Stack.Toolbar>
      {/* Screen content */}
    </>
  );
}

The useImage and image prop pattern for bottom toolbar custom images is iOS-only and is a temporary API that may change in future releases.

Building action menus

For screens with multiple actions, use Stack.Toolbar.Menu to group them into a dropdown menu:

Some Stack.Toolbar.Menu and Stack.Toolbar.MenuAction props are iOS-only. See the API reference for per-prop platform availability.

iOS

import { useState } from 'react';
import { Stack } from 'expo-router';
import { Alert } from 'react-native';

export default function EmailScreen() {
  const [isArchived, setIsArchived] = useState(false);

  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Menu icon="ellipsis.circle">
          <Stack.Toolbar.MenuAction
            icon="arrowshape.turn.up.left"
            onPress={() => Alert.alert('Reply')}>
            Reply
          </Stack.Toolbar.MenuAction>

          <Stack.Toolbar.MenuAction
            icon="arrowshape.turn.up.right"
            onPress={() => Alert.alert('Forward')}>
            Forward
          </Stack.Toolbar.MenuAction>

          <Stack.Toolbar.MenuAction
            icon={isArchived ? 'tray.full' : 'archivebox'}
            isOn={isArchived}
            onPress={() => setIsArchived(!isArchived)}>
            {isArchived ? 'Unarchive' : 'Archive'}
          </Stack.Toolbar.MenuAction>

          <Stack.Toolbar.MenuAction icon="trash" destructive onPress={() => Alert.alert('Delete')}>
            Delete
          </Stack.Toolbar.MenuAction>
        </Stack.Toolbar.Menu>
      </Stack.Toolbar>
      {/* Email content */}
    </>
  );
}

Nested submenus

For more complex menus, nest Stack.Toolbar.Menu inside another menu. Use the inline prop to display submenu items directly without collapsing:

iOS

import { useState } from 'react';
import { Stack } from 'expo-router';

export default function EmailScreen() {
  const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('name');
  const [showHiddenFiles, setShowHiddenFiles] = useState(false);

  return (
    <>
      <Stack.Toolbar>
        <Stack.Toolbar.Menu icon="ellipsis.circle">
          {/* Inline submenu - options appear directly in the menu */}
          <Stack.Toolbar.Menu inline title="Sort By">
            <Stack.Toolbar.MenuAction isOn={sortBy === 'name'} onPress={() => setSortBy('name')}>
              Name
            </Stack.Toolbar.MenuAction>
            <Stack.Toolbar.MenuAction isOn={sortBy === 'date'} onPress={() => setSortBy('date')}>
              Date
            </Stack.Toolbar.MenuAction>
            <Stack.Toolbar.MenuAction isOn={sortBy === 'size'} onPress={() => setSortBy('size')}>
              Size
            </Stack.Toolbar.MenuAction>
          </Stack.Toolbar.Menu>

          {/* Nested submenu - opens as a separate menu */}
          <Stack.Toolbar.Menu title="Preferences">
            <Stack.Toolbar.MenuAction
              isOn={showHiddenFiles}
              onPress={() => setShowHiddenFiles(!showHiddenFiles)}>
              Show Hidden Files
            </Stack.Toolbar.MenuAction>
          </Stack.Toolbar.Menu>
        </Stack.Toolbar.Menu>
      </Stack.Toolbar>
      {/* Email content */}
    </>
  );
}

Using the bottom toolbar

Bottom toolbars are commonly used on iOS for primary screen actions, such as the toolbars in the Photos and Mail apps. To add one, use Stack.Toolbar without a placement prop, it defaults to "bottom":

iOS

import { Stack } from 'expo-router';
import { Alert } from 'react-native';

export default function PhotosScreen() {
  return (
    <>
      <Stack.Toolbar>
        <Stack.Toolbar.Button icon="photo.on.rectangle" onPress={() => Alert.alert('Select')}>
          Select
        </Stack.Toolbar.Button>
        <Stack.Toolbar.Spacer />
        <Stack.Toolbar.Button icon="plus" onPress={() => Alert.alert('Add')}>
          Add
        </Stack.Toolbar.Button>
      </Stack.Toolbar>
    </>
  );
}

Bottom toolbars can only be used inside page components, not in layout files.

Spacers

Use Stack.Toolbar.Spacer to add space between toolbar items. Behavior differs by platform:

  • iOS: a Stack.Toolbar.Spacer without a width creates flexible space between items, pushing them to opposite sides. This is useful for layouts like buttons on both ends of the toolbar. Pass a width for fixed-size spacing.

Adding badges to buttons

In header toolbars, you can add badges to indicate counts or status. Use Stack.Toolbar.Icon, Stack.Toolbar.Label, and Stack.Toolbar.Badge to compose the button content:

import { Stack } from 'expo-router';
import bellIcon from '@/assets/bell.png';

export default function InboxScreen() {
  const unreadCount = 5;

  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Button
          icon={process.env.EXPO_OS === 'ios' ? 'bell' : bellIcon}
          onPress={() => {}}>
          <Stack.Toolbar.Label>Notifications</Stack.Toolbar.Label>
          {unreadCount > 0 && <Stack.Toolbar.Badge>{String(unreadCount)}</Stack.Toolbar.Badge>}
        </Stack.Toolbar.Button>
      </Stack.Toolbar>
      {/* Screen content */}
    </>
  );
}

Badges only work in header placements (left or right), not in the bottom toolbar.

Embedding custom views

When you need something beyond buttons and menus, use Stack.Toolbar.View to embed any React Native component:

import { Stack } from 'expo-router';
import { Pressable, Alert } from 'react-native';
import { SymbolView } from 'expo-symbols';

export default function SearchScreen() {
  return (
    <>
      <Stack.Toolbar>
        <Stack.Toolbar.View>
          <Pressable
            style={{ width: 32, height: 32, justifyContent: 'center', alignItems: 'center' }}
            onPress={() => {
              Alert.alert('Filter pressed');
            }}>
            <SymbolView
              name={{
                ios: 'line.3.horizontal.decrease.circle',
                android: 'filter_list',
              }}
              size={24}
            />
          </Pressable>
        </Stack.Toolbar.View>
      </Stack.Toolbar>
      {/* Screen content */}
    </>
  );
}

Showing and hiding items dynamically

Use the hidden prop to toggle toolbar items based on state:

iOS

import { useState } from 'react';
import { Stack } from 'expo-router';

export default function DocumentScreen() {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <>
      <Stack.Toolbar placement="right">
        <Stack.Toolbar.Button hidden={isEditing} icon="pencil" onPress={() => setIsEditing(true)} />
        <Stack.Toolbar.Button hidden={!isEditing} onPress={() => setIsEditing(false)}>
          Done
        </Stack.Toolbar.Button>
      </Stack.Toolbar>
      {/* Document content */}
    </>
  );
}

Common problems

Liquid glass toolbar buttons flicker in dark mode on iOS 26

Toolbar buttons with liquid glass styling may flicker or flash their background when navigating between screens in dark mode on iOS 26. This happens because the default theme doesn't match the system dark mode, causing visual artifacts in the liquid glass rendering.

To fix this, wrap your root layout with <ThemeProvider> from expo-router using the appropriate theme:

import { ThemeProvider, DarkTheme, DefaultTheme, Stack } from 'expo-router';
import { useColorScheme } from 'react-native';

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack />
    </ThemeProvider>
  );
}

White background flashes when navigating between screens

A white flash between screen transitions usually means the navigation stack is using a light background while your app uses a dark theme. This is especially noticeable when screens contain toolbar items, as the flash contrasts with the toolbar styling.

To fix this, wrap your root layout with Expo Router's <ThemeProvider> and pass the appropriate theme:

import { ThemeProvider, DarkTheme, DefaultTheme, Stack } from 'expo-router';
import { useColorScheme } from 'react-native';

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack />
    </ThemeProvider>
  );
}

Large title does not collapse when scrolling

When using headerLargeTitle: true (or <Stack.Title large>) alongside Stack.Toolbar, the large title may not collapse on scroll. This happens when the scrollable view is not the direct first child of the screen component.

To fix this, ensure ScrollView or FlatList is the first child rendered by your screen component. If you need a wrapper, set collapsable={false} on it:

import { Stack } from 'expo-router';
import { ScrollView, View, Text } from 'react-native';

export default function Home() {
  return (
    <ScrollView>
      <Stack.Title large>Home</Stack.Title>
      <Text>Content here</Text>
    </ScrollView>
  );
}

If you need to wrap the ScrollView, set collapsable={false} on the wrapper:

import { Stack } from 'expo-router';
import { ScrollView, View, Text } from 'react-native';

export default function Home() {
  return (
    <View collapsable={false}>
      <ScrollView>
        <Stack.Title large>Home</Stack.Title>
        <Text>Content here</Text>
      </ScrollView>
    </View>
  );
}

Known limitations

Native only

Stack.Toolbar only renders on Android and iOS. Web has no standard toolbar, so you need to implement your own if you need toolbar behavior there.

Bottom toolbar only in page components

The bottom toolbar can only be used inside page components, not in layout files. This is because the bottom toolbar needs to be associated with a specific screen's content.

Cannot nest toolbars

You cannot nest Stack.Toolbar components inside each other.

Badge only in header placements

Stack.Toolbar.Badge is only supported when using placement="left" or placement="right". Badges are not displayed in the bottom toolbar.

Learn more

For complete API documentation, including all available props, see the Stack.Toolbar API reference.

Sources

The original Expo docs for this page:

  • Stack Toolbar (source-of-truth/router/advanced/stack-toolbar.md)

On this page