Side menu
What a side menu is, how to build one, how it opens and closes, how a pick in the menu changes the content, and how it looks on each platform.
A side menu is a drawer beside the content of the app: the sections, the account, the settings, out of the way until the user pulls it in. SideMenu builds one from the content, its center, and a menu on a side:
import { createNavigation, SideMenu } from '@apexnative/navigation';
import { MenuScreen } from './screens/Menu';
import { HomeScreen } from './screens/Home';
import { SettingsScreen } from './screens/Settings';
export const Navigation = createNavigation({
screens: {
Menu: MenuScreen,
Home: HomeScreen,
Settings: SettingsScreen,
},
root: SideMenu({
left: 'Menu', // the menu
center: 'Home', // the content
}),
});
Home shows. A swipe from the left edge brings Menu in over it.
This page answers:
- What a side menu is, and its parts.
- How to build one.
- How it opens and closes.
- How a pick in the menu changes the content.
- How it looks on each platform, and where the swipe is off.
What a side menu is
A side menu has a center and a menu on one side or both. The center is the app the user works in; the menu slides in over it, or moves it aside, and goes away again.
- The center is a stack, so it pushes as the root does: a screen named alone gets a stack.
- A menu is a screen on its own, without a stack: a list of what the center can show.
Building a side menu
SideMenu takes its parts, each a screen of screens by its name or a layout. The side methods take each menu's settings, such as its width:
SideMenu({ left: 'Menu', center: 'Home' }).left({ width: 300 });
A right menu is the same: right: 'Filters', with .right({ … }). A side menu is a root most of the time, root: main or one of roots. It can also be a tab or the content of a modal.
Opening and closing
The user opens a menu with a swipe from its edge. A swipe back, a tap on the content or the system back on Android closes it. The app does it with openMenu and closeMenu, such as from a button of the top bar:
import { defineScreen } from '@apexnative/navigation';
export const HomeScreen = defineScreen({
component: Home,
options: (s) =>
s.leftButtons({
id: 'menu',
icon: require('../icons/menu.png'),
onPress: ({ navigation }) => navigation.openMenu('left'),
}),
});
A tap on the button opens the menu. A button of the options gets the navigation of its screen in onPress, so the options need no import of navigation.
closeMenu() closes whichever menu is open. Both resolve once the menu has moved.
A pick in the menu
A menu is a screen without a stack, so its commands act on the center, and the menu closes as the center changes: it starts to slide away at the tap, and a push slides in as it goes. The command resolves once both are done. A pick usually switches the section, so the center starts over with it:
import { Button, View } from 'react-native';
import { useNavigation } from '@apexnative/navigation';
function Menu() {
const navigation = useNavigation();
const open = (section: 'Home' | 'Settings') =>
navigation.replace(section);
return (
<View>
<Button title="Home" onPress={() => open('Home')} />
<Button title="Settings" onPress={() => open('Settings')} />
</View>
);
}
A tap on Settings closes the menu and shows Settings in the center as it goes. Which command to use depends on the way back:
| From the menu | The center |
|---|---|
replace('Settings') | Shows Settings alone: a new section, with no back to the one before. |
push('Profile') | Pushes Profile over its top screen, with back to it. |
How it looks
| Platform | The menu |
|---|---|
| iOS | Over the content, which dims: openMode: 'aboveContent' of the side, the default. Or under it, with the content moved aside: 'pushContent'. Then animation(…) says how the menu appears: 'parallax', 'slide', 'slide-and-scale' or 'door'. |
| Android | The drawer of the platform: over the content, which dims. |
The menu's color is its screen's layout.backgroundColor: it fills the whole height of the menu, behind the status bar and the navigation bar, while the menu's content keeps its top and sides clear of them (the bottom goes down to the edge: a list adds insets.bottom). A background on the React content stops at the safe area.
.ios() keeps the settings that only iOS has apart from the others:
const main = SideMenu({ left: 'Menu', center: 'Home' })
.left({ width: 300 })
.ios((menu) =>
menu.left({ openMode: 'pushContent' }).animation('parallax'),
);
On iOS the content moves aside, and the menu comes in under it with a parallax; on Android the drawer slides over the content.
iOS 26: left({ glass: true }) puts the menu on Liquid Glass. The glass shows where the menu's screen has no background of its own.
When the menus are created
The screens of the menus are created with the side menu by default, with their React content. A heavy menu competes
with the first render of the center. options({ menusAttachMode }) moves the creation:
SideMenu({ left: 'Menu', center: 'Home' }).options({ menusAttachMode: 'onOpen' });
'together'(the default): with the center.'afterCenter': right after the side menu first appears. The menus don't compete with the first render of the center and are ready at the first open.'onOpen': each menu the first time it starts to open, from a swipe,openMenu,visible: trueorvisiblein the layout. Until React draws the menu, it shows its background (backgroundColor). A second open reuses it.
The trade-off of 'onOpen': the first open of a menu doesn't wait for its render and shows its background for a moment, while a menu not opened yet runs no code. A push into the stack of such a menu and mergeOptions on its screen wait for the menu, like a tab not yet selected. The screens of the menus get appear events only when the menu opens, in every mode. The mode is read once, when the side menu is created, from its own options, else the default options; mergeOptions doesn't change it. With appProvider the screens render in the app's React tree at once in every mode, and the mode delays only their native containers. It means the same on iOS and Android.
Where the swipe is off
A swipe from the edge can clash with the content, such as a map or a carousel. A screen turns it off while it is on top of the center, in its options:
export const MapScreen = defineScreen({
component: Map,
options: (s) => s.sideMenu({ left: { enabled: false } }),
});
On the map the swipe pans the map, and the menu still opens with openMenu. On iOS, gesture('entireScreen') lets the swipe start anywhere on the content, 'bezel' only at the edge (the default).