Skip to main content

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​

VariantWhere it rendersContainerDismissal
'modal' (default)Through Portal, over the content, from the start edge behind a 32% scrimsurfaceContainerLow, elevation level 1, 16dp end cornersScrim press, Android back, Escape on web, and a destination press
'standard'In place, as a permanent panel at the start of a rowsurface, elevation level 0, no roundingNone; 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. icon is optional; MD3 allows text-only destinations in a label section. selectedIcon swaps in while active. badge is a trailing text such as an unread count, per the MD3 drawer anatomy — it is not announced, so add it to accessibilityLabel.
  • NavigationDrawer.Section — a group with an optional headline in titleSmall, 56dp tall and inset 28dp from the sheet edge. On web the headline renders as <h2>; set headingLevel to fit the page outline.
  • NavigationDrawer.Divider — a Divider inset 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​

PropTarget
containerColorThe drawer background
contentColorIcon, label and badge of inactive destinations, and section headlines
selectedContentColorIcon, label and badge of the active destination; the state layers re-derive from it
indicatorColorThe active indicator
labelStyleEvery destination's label Text; an item's own labelStyle merges over it
styleThe drawer surface
scrimStyleThe 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​

TokenValue
Sheet360dp wide; modal surfaceContainerLow at level 1 with 16dp end corners, standard surface at level 0
Scrimscrim at 32%
Item56dp tall, 336dp wide (12dp inset each side), full corners, 16dp start and 24dp end padding, 12dp icon-to-label gap
Active indicatorsecondaryContainer
Icon24dp — active onSecondaryContainer, inactive onSurfaceVariant
Label and badgelabelLarge — active onSecondaryContainer, inactive onSurfaceVariant
HeadlinetitleSmall, onSurfaceVariant, 56dp tall, 28dp inset
Divider1dp outlineVariant, 28dp inset each side

Accessibility​

  • A modal drawer is a dialog with aria-modal; a standard drawer is navigation. Pass accessibilityLabel to name it ("Main navigation").
  • The destinations are grouped as a tablist; each item reports role="tab" with accessibilityState.selected and, 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 dismissable modal drawer.

Props​

PropTypeDefaultRequiredDescription
childrenReactNode-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.
valuestring-NoValue of the active destination (controlled).
defaultValuestring-NoValue of the initially active destination (uncontrolled). Omit it for a drawer with no active destination.
onValueChange(value: string) => void-NoCalled with the value of the destination that was pressed.
insetTopbooleantrue for 'modal', false for 'standard'NoWhether the drawer's content is padded by the top safe-area inset. The container color still extends under the inset.
insetBottombooleantrue for 'modal', false for 'standard'NoWhether the drawer's content is padded by the bottom safe-area inset.
containerColorstringsurfaceContainerLow for 'modal', surface for 'standard'NoOverride the drawer background.
contentColorstringonSurfaceVariantNoOverride the icon, label, and badge color of inactive destinations, and the section headlines.
selectedContentColorstringonSecondaryContainerNoOverride the icon, label, and badge color of the active destination. State-layer colors are derived from it automatically.
indicatorColorstringsecondaryContainerNoOverride the active indicator color.
labelStyleStyleProp<TextStyle>-NoStyle applied to every destination's label `Text` — does not affect the icons or the badges.
styleStyleProp<ViewStyle>-NoStyle applied to the drawer surface.
accessibilityLabelstring-NoScreen-reader label for the drawer. A modal drawer is announced as a dialog, so it needs a name such as "Main navigation".
testIDstring-NoTest 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; }-NoWeb 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-
variantenum'modal'No-
visibleboolean-NoWhether the drawer is shown. Exit animations run before it unmounts.
onDismiss() => void-NoCalled 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.
dismissablebooleantrueNoWhether the scrim press, the Android back button, and Escape dismiss the drawer.
dismissOnSelectbooleantrueNoWhether pressing a destination also calls `onDismiss`. MD3 closes a modal drawer once a destination is chosen.
hostNamestring-NoRender into a specific named `PortalHost`.
scrimStyleStyleProp<ViewStyle>-NoStyle applied to the scrim.
scrimAccessibilityLabelstring'Close navigation drawer'NoScreen-reader label for the scrim's dismiss action.
PropTypeDefaultRequiredDescription
valuestring-YesStable identifier used for selection state and the change callback.
labelstring-YesText label. One line — a label that doesn't fit is truncated.
iconIconSource-NoLeading 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.
selectedIconIconSource-NoIcon shown while the destination is active — MD3 pairs an outlined resting icon with its filled counterpart. Falls back to `icon`.
badgestring | number-NoTrailing 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-NoCalled when the destination is pressed, after the drawer's `onValueChange`.
disabledbooleanNoGreys the destination out at 38% and stops it responding.
labelStyleStyleProp<TextStyle>-NoStyle applied to this destination's label `Text`. Merged over the drawer's `labelStyle`.
styleStyleProp<ViewStyle>-NoStyle applied to the destination row.
accessibilityLabelstring-NoScreen-reader label. Defaults to `label`.
testIDstring-NoTest id applied to the row. The indicator carries `<testID>-indicator`.
hrefAttrs{ target?: string; rel?: string; download?: string | boolean; }-NoWeb 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-
PropTypeDefaultRequiredDescription
headlinestring-NoSection headline in `titleSmall`, above the destinations. Omit it for a group with no title.
headingLevelenum2NoThe outline level of the headline on web, where it renders as `<hN>`. Native has no heading levels and ignores it.
childrenReactNode-NoThe section's `NavigationDrawer.Item`s.
headlineStyleStyleProp<TextStyle>-NoStyle applied to the headline `Text`.
styleStyleProp<ViewStyle>-NoStyle applied to the section container.
hrefAttrs{ target?: string; rel?: string; download?: string | boolean; }-NoWeb 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-
PropTypeDefaultRequiredDescription
styleStyleProp<ViewStyle>-No-
hrefAttrs{ target?: string; rel?: string; download?: string | boolean; }-NoWeb 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-
thicknessnumber1NoLine thickness in dp.
containerColorstringtheme.colors.outlineVariantNoOverride the line color.