Skip to main content

Top bar

The bar at the top of a stack: the title and the subtitle, the large title, the background and the border, hiding the bar, a component in place of the title, and the title menu.

SectionTop barDraftDocumented, not reviewed yet: the API can change.

Every screen of a stack has a bar at the top: it names the screen and holds the way back and the actions. A screen fills the bar through its options, and the title is the one most apps set:

src/screens/Inbox.tsx
import { defineScreen } from '@apexnative/navigation';

export const InboxScreen = defineScreen({
component: Inbox,
options: (s) => s.title('Inbox'),
});

The bar shows "Inbox" as the screen comes in, before React renders it. This page answers:

  1. What the top bar is, and its parts.
  2. The title and the subtitle.
  3. The large title.
  4. The background and the border.
  5. Hiding the bar.
  6. A component in place of the title, and the title menu.
  7. The bar on iPhone Duo.

The buttons of the bar have a page of their own: Bar buttons. So do the back button (Back and gestures) and the search field (Search).

What the top bar is​

The top bar belongs to the stack, not to a screen: one bar for all the screens of the stack. Each screen gives it its title and buttons through its options, and the bar changes with the screen as it comes in, as in any native app. The back button shows by itself on every screen but the first.

iOS
Android

The platform draws the bar: UINavigationBar on iOS, with Liquid Glass on iOS 26, and the Material 3 top app bar on Android. Its colors come from the theme until a screen sets its own.

The glass design and the compatibility mode​

On iOS the look of iOS 26 (Liquid Glass) is the glass design, and this page and the bar buttons call it that. An app in the compatibility mode (UIDesignRequiresCompatibility = YES in Info.plist) doesn't get it: the mode applies on iOS 26, and on iOS 27 only to an app built with an SDK before iOS 27. There the bar looks and behaves as before iOS 26: a title with a subtitle as two lines only when no large title can show (with a large title the subtitle shows nowhere), no capsules (title.glass, hideSharedBackground and group do nothing), style: 'prominent' is a bold Done button, the back button shows the title of the screen below, background.color gives an opaque bar, and a large title is drawn in the bar. The bottom tabs follow the same mode.

The title​

The title is the name of the screen. The subtitle is a second line under it, such as a status or a count. Both can come from the props of the screen:

export const ChatScreen = defineScreen({
component: Chat,
options: (s, props) => s.title(props.name).subtitle('online'),
});

A title can change while the screen shows, such as when its data has loaded: see Changing the options of a shown screen.

The look of the text goes in an object instead of the string:

s.title({ text: 'Chat', color: '#1E2A44', fontFamily: 'Righteous' });

fontSize, fontWeight and fontStyle go in the same object, and the subtitle takes the same keys.

PlatformThe title
iOSIn the middle of the bar. On iOS 26, glass: 'regular' or 'clear' puts it on a capsule of Liquid Glass, as the buttons are, with the subtitle as a second line.
AndroidAt the leading edge, next to the back arrow. alignment: 'center' puts it in the middle.

The large title​

iOS A large title is big at the top of the screen, under the row of the bar. It goes into the bar as the content scrolls, so a long list keeps its room:

iOS
Android
s.title('Inbox').largeTitle();

largeTitle() shows it on this screen. A screen pushed from it usually has a small title, so it sets its own displayMode:

s.title('Details').largeTitle({ displayMode: 'never' });
displayModeThe large title
'always'Shows on this screen. The default of largeTitle().
'never'Doesn't show on this screen.
'automatic'As on the screen under it; the first screen of a stack shows it.
'inline'iOS 17+: in the row of the bar instead of under it. Only on a screen without a back button, such as the first of a stack or of a modal: next to a back button UIKit shows it as 'always'.

collapseOnScroll: false keeps the large title where it is: the list scrolls under the bar and the title stays.

Android The same option shows the large top app bar of Material 3: the title big under the row of the bar, fading into the row as the content scrolls. The screen goes under the bar, so its list keeps its first rows clear with useSafeAreaInsets().top (see The safe area). 'inline' keeps the title in the row, as 'never' does. Under a large title hideOnScroll doesn't work.

The background and the border​

With the glass design the bar is Liquid Glass, and the content shows through it as it scrolls under. A color takes the glass away, so set one only when the design needs it:

s.topBar({ background: { color: '#F3E9D2' }, noBorder: true });

background.color colors the bar on both platforms. With a large title and the glass design the color goes behind the title in the list, and fills the bar once the title collapses. Under a bar with a color the list has no edge effect at the top: see The edge of the scroll.

The other keys of the background are rarer:

OptionWhat it does
background.componentA component of screens drawn behind the bar, such as a gradient. It is registered as the title component is.
background.translucentiOS true: the content shows through; Liquid Glass with the glass design, where it is the default. false: a solid bar, white in the light theme and black in the dark one.
background.bluriOS The system's frosted blur, plainer than the glass. A color wins over it, and it wins over translucent.
scrollEdgeAppearanceiOS The look of the bar while the content is at its top, with active: true. The bar takes its usual look once the content scrolls under it.

The line under the bar:

OptionWhat it does
noBorderiOS No line under the bar.
borderColorThe color of the line.
elevation, borderHeightAndroid The shadow and the line under the bar, in dp. elevation: 0 takes the shadow away.

Hiding the bar​

A screen without a bar, such as a photo or a sign-in, hides it in its options. The bar comes back when the user goes back, because the screen under it has its own options:

iOS
Android
s.hideTopBar();

hideTopBar(hidden) takes a value too, so the state can decide, as in a viewer that hides its bar on a tap:

useScreenOptions((s) => s.hideTopBar(fullScreen));

The bar can also hide and show by itself:

OptionThe bar
hideOnScrollHides while the content scrolls down, and comes back as it scrolls up.
hideOnTapiOS Hides and shows on a tap on the screen.
hideOnKeyboardiOS Hides while the keyboard shows.
animatefalse: hides and shows at once, without the animation.

With hideOnScroll the screen goes under the bar, so its rows show where the bar was. Its list keeps its first rows clear: on iOS with contentInsetAdjustmentBehavior="automatic", on Android with paddingTop: useSafeAreaInsets().top. The bottom edge is not padded by default, so the list also adds useSafeAreaInsets().bottom to its bottom padding (an iOS list with contentInsetAdjustmentBehavior="automatic" does it itself).

drawBehind: true keeps the bar and puts the content under it, as a map or a photo under a glass bar.

A component in place of the title​

A chat shows an avatar, a name and a status where the title is. Text can't do that, so a React component takes the place of the title. It takes three steps.

1. The component, a plain React component with its props:

src/bar/ChatTitle.tsx
import { Image, Text, View } from 'react-native';

type Props = { name: string; avatar: string };

export function ChatTitle(props: Props) {
return (
<View style={{ flexDirection: 'row', gap: 8 }}>
<Image
source={{ uri: props.avatar }}
style={{ width: 28, height: 28, borderRadius: 14 }}
/>
<View>
<Text style={{ fontWeight: '600' }}>{props.name}</Text>
<Text style={{ fontSize: 12, opacity: 0.6 }}>online</Text>
</View>
</View>
);
}

2. Its registration in screens, as any screen. The bar renders it in a React root of its own, so it needs a name:

Navigation.ts
export const Navigation = createNavigation({
screens: { Inbox: InboxScreen, Chat: ChatScreen, ChatTitle },
root: 'Inbox',
});

3. The options of the screen name it in title.component, with its props:

src/screens/Chat.tsx
export const ChatScreen = defineScreen({
component: Chat,
options: (s, props) =>
s.title({
component: {
name: 'ChatTitle',
passProps: { name: props.name, avatar: props.avatar },
},
}),
});

The screen comes in with the avatar and the name in the bar: it waits for the component to draw, so the title doesn't pop in after it.

alignment: 'fill' in component gives the component all the room between the side buttons, from the back button on, so its content can start at the leading edge. Without it the component sits in the middle.

The same three steps give the bar a background of its own, with background.component, and a button: see A component as a button.

The title menu​

iOS 16+ A tap on the title can open a menu, as the title of a document does in system apps; UIKit shows a chevron next to the title. A pick sends a button press with the id of the item:

iOS
src/screens/Draft.tsx
export const DraftScreen = defineScreen({
component: Draft,
options: (s) =>
s.title({
text: 'Draft',
menu: { items: [{ id: 'duplicate', title: 'Duplicate' }] },
}),
});

function Draft() {
useNavigationButton('duplicate', () => duplicateDraft()); // your own
// …
}

rename: true adds Rename to the menu: the title turns into a field in the bar, and the new title comes as the event titleRenamed:

useScreenEvent('titleRenamed', ({ text }) => renameDraft(text));

document: { url } heads the menu with a file: its icon, name and type. A large title shows no chevron and opens no menu. On Android the title has no menu: put the same actions in the overflow menu.

The layout of the bar​

This section is rarely needed: most apps keep the platform's layout.

iOS 16+ style lays the bar out. 'navigator', the default, centers the title, as a stack of screens does. 'browser' and 'editor' put it next to the back button, as an app of documents does, and leave room for center buttons on iPad.

Android height and topMargin, of the bar and of its title, size the bar for a design of its own.

The bar on iPhone Duo​

iOS 27.1 On iPhone Duo, folded and open, the title stays in the row at the top, and the large title too. The system moves the buttons into a vertical bar at the trailing edge, with the status bar and the tab bar: see Buttons on iPhone Duo. The app's options stay the same; a screen that needs its bars at the top asks for them:

s.verticalBars(false); // the bars at the top, as on any iPhone

Set it once for a screen: Apple asks apps not to switch it with the state of the screen. In a pair the bar is the primary's, so is this option.

The screen goes under the vertical bar, as under the top bar, and its list keeps its rows clear: see The vertical bar of iPhone Duo. Two things UIKit does there, which an app sees as they are:

  • The bar goes away on a scroll down. iOS 27 minimizes the navigation bar as the content scrolls down: the whole bar, its title and its blur, leaves, and comes back on a scroll up (UIKit's navigationItem.navigationBarMinimization). The library has no option for it yet.
  • Folded, a bar with a background of its own leaves a band above it. UIKit puts the bar about 24 points below the top of the outer display, so with topBar.background a band of about 24 points shows above the bar's color.

API​