NavigationDrawer
Navigation drawers hold an app's destinations in a side panel: modal over the content on a compact or medium window, or standing beside it on an expanded one. Follows the Material Design 3 Navigation drawer specification.
Destinations are 56dp rows with a 24dp icon, a labelLarge label, and an optional badge text,
grouped in sections with a titleSmall headline. The active one sits on a full-corner
secondaryContainer indicator. The sheet is 360dp wide.
The modal variant renders through Portal, so your app must be wrapped in a
PortalHost once at the root. It claims the PORTAL_LAYERS.sheet layer with the bottom sheet.
Usage
Compose the drawer from NavigationDrawer.Section, NavigationDrawer.Item and
NavigationDrawer.Divider. The drawer owns the selection: an item is active when its value
matches the drawer's value.
import { Button, NavigationDrawer, PortalHost, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { Text, View } from 'react-native'
function Screen() {
const [open, setOpen] = useState(false)
const [destination, setDestination] = useState('inbox')
return (
<View style={{ flex: 1, padding: 24, gap: 16 }}>
<Button variant="tonal" onPress={() => setOpen(true)}>Open drawer</Button>
<Text>Active: {destination}</Text>
<NavigationDrawer
visible={open}
onDismiss={() => setOpen(false)}
value={destination}
onValueChange={setDestination}
accessibilityLabel="Main navigation"
>
<Typography variant="titleLarge" style={{ paddingHorizontal: 16, paddingVertical: 18 }}>
Mail
</Typography>
<NavigationDrawer.Section headline="Mail">
<NavigationDrawer.Item value="inbox" label="Inbox" icon="inbox-outline" selectedIcon="inbox" badge={24} />
<NavigationDrawer.Item value="outbox" label="Outbox" icon="send-outline" />
<NavigationDrawer.Item value="trash" label="Trash" icon="trash-can-outline" />
</NavigationDrawer.Section>
<NavigationDrawer.Divider />
<NavigationDrawer.Section headline="Labels">
<NavigationDrawer.Item value="family" label="Family" />
<NavigationDrawer.Item value="work" label="Work" />
</NavigationDrawer.Section>
</NavigationDrawer>
</View>
)
}
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}
Anything that is not a section, an item or a divider renders as is, so a Typography headline
or an account block goes straight in as a child.
Variants
| Variant | Where it renders | Container | Dismissal |
|---|---|---|---|
'modal' (default) | Through Portal, over the content, from the start edge behind a 32% scrim | surfaceContainerLow, elevation level 1, 16dp end corners | Scrim press, Android back, Escape on web, and a destination press |
'standard' | In place, as a permanent panel at the start of a row | surface, elevation level 0, no rounding | None; it is always open |
A modal drawer takes visible and onDismiss. A standard drawer takes neither, and the type
rejects visible, onDismiss, dismissable, dismissOnSelect, hostName, scrimStyle and
scrimAccessibilityLabel on it.
<NavigationDrawer variant="standard" value={destination} onValueChange={setDestination}>
<NavigationDrawer.Item value="inbox" label="Inbox" icon="inbox" />
</NavigationDrawer>
Selection
value + onValueChange is the controlled form; leave value off and the drawer tracks the
selection itself, starting on defaultValue. Unlike the bar and the rail, a drawer may have no
active destination: omit defaultValue for that.
A modal drawer calls onDismiss after a destination press, because MD3 closes the drawer once a
destination is chosen. Pass dismissOnSelect={false} to keep it open. An item's own onPress
fires after the drawer's onValueChange.
Items, sections and dividers
NavigationDrawer.Item— a destination.iconis optional; MD3 allows text-only destinations in a label section.selectedIconswaps in while active.badgeis a trailing text such as an unread count, per the MD3 drawer anatomy — it is not announced, so add it toaccessibilityLabel.NavigationDrawer.Section— a group with an optionalheadlineintitleSmall, 56dp tall and inset 28dp from the sheet edge. On web the headline renders as<h2>; setheadingLevelto fit the page outline.NavigationDrawer.Divider— aDividerinset 28dp from both sheet edges, between sections.
Safe area
A modal drawer spans the full screen height, so by default it pads its content by the top and
bottom insets while the container color extends under them. A standard drawer defaults both
off, because the Layout around it usually owns the edges. insetTop and insetBottom
override either default.
In a navigator drawer
A drawer navigator, from expo-router/drawer or @react-navigation/drawer,
brings its own sheet and renders your drawerContent in it. Neither variant
fits that host as it is. 'modal' brings a second scrim and sheet, and
'standard' is a 360dp panel, wider than the navigator sheet on a phone. Use
'standard' at the full width of the sheet, and keep the top inset:
<Drawer
drawerContent={() => (
<NavigationDrawer variant="standard" insetTop style={{ width: '100%' }}>
<NavigationDrawer.Item value="inbox" label="Inbox" icon="inbox" />
</NavigationDrawer>
)}
/>
When the drawer is only a menu of actions, it needs no navigator. A modal
NavigationDrawer that a button opens does the same work.
Overrides
| Prop | Target |
|---|---|
containerColor | The drawer background |
contentColor | Icon, label and badge of inactive destinations, and section headlines |
selectedContentColor | Icon, label and badge of the active destination; the state layers re-derive from it |
indicatorColor | The active indicator |
labelStyle | Every destination's label Text; an item's own labelStyle merges over it |
style | The drawer surface |
scrimStyle | The scrim (modal only) |
Disabled destinations always use the MD3 disabled treatment (38% content); no override changes that.
When you pass testID, a modal drawer's scrim carries <testID>-scrim. Each item takes its own
testID, and its indicator carries <testID>-indicator.
Tokens
| Token | Value |
|---|---|
| Sheet | 360dp wide; modal surfaceContainerLow at level 1 with 16dp end corners, standard surface at level 0 |
| Scrim | scrim at 32% |
| Item | 56dp tall, 336dp wide (12dp inset each side), full corners, 16dp start and 24dp end padding, 12dp icon-to-label gap |
| Active indicator | secondaryContainer |
| Icon | 24dp — active onSecondaryContainer, inactive onSurfaceVariant |
| Label and badge | labelLarge — active onSecondaryContainer, inactive onSurfaceVariant |
| Headline | titleSmall, onSurfaceVariant, 56dp tall, 28dp inset |
| Divider | 1dp outlineVariant, 28dp inset each side |
Accessibility
- A modal drawer is a
dialogwitharia-modal; a standard drawer isnavigation. PassaccessibilityLabelto name it ("Main navigation"). - The destinations are grouped as a
tablist; each item reportsrole="tab"withaccessibilityState.selectedand, when disabled,accessibilityState.disabled. - A section headline is a
heading. - On web, a modal drawer traps keyboard focus: focus moves into it on open, Tab cycles inside
it, Escape dismisses it when
dismissable, and focus returns to the trigger on close. - On Android, the hardware back button dismisses a
dismissablemodal drawer.
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | No | `NavigationDrawer.Item`s, grouped with `NavigationDrawer.Section` and separated with `NavigationDrawer.Divider`. Arbitrary content, such as a `Typography` headline at the top, is rendered as is. |
value | string | - | No | Value of the active destination (controlled). |
defaultValue | string | - | No | Value of the initially active destination (uncontrolled). Omit it for a drawer with no active destination. |
onValueChange | (value: string) => void | - | No | Called with the value of the destination that was pressed. |
insetTop | boolean | true for 'modal', false for 'standard' | No | Whether the drawer's content is padded by the top safe-area inset. The container color still extends under the inset. |
insetBottom | boolean | true for 'modal', false for 'standard' | No | Whether the drawer's content is padded by the bottom safe-area inset. |
containerColor | string | surfaceContainerLow for 'modal', surface for 'standard' | No | Override the drawer background. |
contentColor | string | onSurfaceVariant | No | Override the icon, label, and badge color of inactive destinations, and the section headlines. |
selectedContentColor | string | onSecondaryContainer | No | Override the icon, label, and badge color of the active destination. State-layer colors are derived from it automatically. |
indicatorColor | string | secondaryContainer | No | Override the active indicator color. |
labelStyle | StyleProp<TextStyle> | - | No | Style applied to every destination's label `Text` — does not affect the icons or the badges. |
style | StyleProp<ViewStyle> | - | No | Style applied to the drawer surface. |
accessibilityLabel | string | - | No | Screen-reader label for the drawer. A modal drawer is announced as a dialog, so it needs a name such as "Main navigation". |
testID | string | - | No | Test id applied to the drawer surface. A modal drawer's scrim carries `<testID>-scrim`. Each `NavigationDrawer.Item` takes its own `testID`. |
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 | - |
variant | enum | 'modal' | No | - |
visible | boolean | - | No | Whether the drawer is shown. Exit animations run before it unmounts. |
onDismiss | () => void | - | No | Called when the user dismisses the drawer — a scrim press, the Android back button, Escape on web, or a destination press when `dismissOnSelect` is on. Set `visible` to `false` in response. |
dismissable | boolean | true | No | Whether the scrim press, the Android back button, and Escape dismiss the drawer. |
dismissOnSelect | boolean | true | No | Whether pressing a destination also calls `onDismiss`. MD3 closes a modal drawer once a destination is chosen. |
hostName | string | - | No | Render into a specific named `PortalHost`. |
scrimStyle | StyleProp<ViewStyle> | - | No | Style applied to the scrim. |
scrimAccessibilityLabel | string | 'Close navigation drawer' | No | Screen-reader label for the scrim's dismiss action. |
NavigationDrawer.Item
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
value | string | - | Yes | Stable identifier used for selection state and the change callback. |
label | string | - | Yes | Text label. One line — a label that doesn't fit is truncated. |
icon | IconSource | - | No | Leading icon at 24dp. Accepts a string name (resolved via the theme's `iconResolver`), a pre-rendered element, or a render function. MD3 allows a drawer destination with no icon. |
selectedIcon | IconSource | - | No | Icon shown while the destination is active — MD3 pairs an outlined resting icon with its filled counterpart. Falls back to `icon`. |
badge | string | number | - | No | Trailing badge text, such as an unread count. Rendered as text at the end of the row, per the MD3 drawer anatomy. Add it to `accessibilityLabel` so a screen reader announces it. |
onPress | () => void | - | No | Called when the destination is pressed, after the drawer's `onValueChange`. |
disabled | boolean | | No | Greys the destination out at 38% and stops it responding. |
labelStyle | StyleProp<TextStyle> | - | No | Style applied to this destination's label `Text`. Merged over the drawer's `labelStyle`. |
style | StyleProp<ViewStyle> | - | No | Style applied to the destination row. |
accessibilityLabel | string | - | No | Screen-reader label. Defaults to `label`. |
testID | string | - | No | Test id applied to the row. The indicator carries `<testID>-indicator`. |
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: PressableKeyDownEvent) => void | - | No | - |
NavigationDrawer.Section
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
headline | string | - | No | Section headline in `titleSmall`, above the destinations. Omit it for a group with no title. |
headingLevel | enum | 2 | No | The outline level of the headline on web, where it renders as `<hN>`. Native has no heading levels and ignores it. |
children | ReactNode | - | No | The section's `NavigationDrawer.Item`s. |
headlineStyle | StyleProp<TextStyle> | - | No | Style applied to the headline `Text`. |
style | StyleProp<ViewStyle> | - | No | Style applied to the section 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 | - |
NavigationDrawer.Divider
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
style | StyleProp<ViewStyle> | - | No | - |
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 | - |
thickness | number | 1 | No | Line thickness in dp. |
containerColor | string | theme.colors.outlineVariant | No | Override the line color. |