NavigationDrawer in Compose: Modal & Permanent Side Menu Guide
Navigation drawers provide organized menu navigation, especially valuable on tablets. This guide covers ModalNavigationDrawer, PermanentNavigationDrawer, and adaptive layouts.
ModalNavigationDrawer: Phone-Style Drawer
The ModalNavigationDrawer slides from the left, overlaying content:
var drawerState by remember { mutableStateOf(DrawerState(DrawerValue.Closed)) }
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
Text("Menu", modifier = Modifier.padding(16.dp))
Divider()
NavigationDrawerItem(
label = { Text("Home") },
selected = true,
onClick = { }
)
NavigationDrawerItem(
label = { Text("Profile") },
selected = false,
onClick = { }
)
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("App") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
}
)
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
Text("Main content")
}
}
}
rememberDrawerState: Managing State
Control drawer open/closed state:
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = { /* ... */ }
) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
scope.launch {
drawerState.open()
}
}
) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
}
)
}
) { /* ... */ }
}
PermanentNavigationDrawer: Tablet-Style Layout
For wider screens, use PermanentNavigationDrawer that stays visible:
@Composable
fun TabletLayout() {
Row(modifier = Modifier.fillMaxSize()) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet {
Text("Menu", modifier = Modifier.padding(16.dp))
Divider()
NavigationDrawerItem(
label = { Text("Home") },
selected = true,
onClick = { }
)
NavigationDrawerItem(
label = { Text("Settings") },
selected = false,
onClick = { }
)
}
}
) {
Box(modifier = Modifier.weight(1f)) {
Text("Main content")
}
}
}
}
NavigationDrawerItem Customization
Customize drawer items with icons and badges:
NavigationDrawerItem(
label = { Text("Notifications") },
icon = { Icon(Icons.Default.Notifications, contentDescription = null) },
selected = isSelected,
onClick = { },
badge = { Badge { Text("5") } },
modifier = Modifier.padding(horizontal = 12.dp)
)
Adaptive Drawer with WindowSizeClass
Switch between modal and permanent based on screen size:
@Composable
fun AdaptiveNavigationDrawer(windowSizeClass: WindowSizeClass) {
when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> {
// Use ModalNavigationDrawer for phones/small tablets
ModalNavigationDrawer(
drawerContent = { /* ... */ }
) { /* content */ }
}
else -> {
// Use PermanentNavigationDrawer for large tablets
Row {
PermanentNavigationDrawer(
drawerContent = { /* ... */ }
) { /* content */ }
}
}
}
}
User Profile Header in Drawer
Add profile section at the top of the drawer:
@Composable
fun DrawerHeader(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Surface(
shape = CircleShape,
modifier = Modifier.size(48.dp)
) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = "Profile",
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "John Doe",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "john@example.com",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
)
}
}
Complete Example
@Composable
fun AppWithDrawer() {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var selectedItem by remember { mutableStateOf("Home") }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
DrawerHeader()
Divider()
listOf("Home", "Profile", "Settings").forEach { item ->
NavigationDrawerItem(
label = { Text(item) },
selected = item == selectedItem,
onClick = {
selectedItem = item
scope.launch { drawerState.close() }
}
)
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(selectedItem) },
navigationIcon = {
IconButton(
onClick = { scope.launch { drawerState.open() } }
) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
}
)
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
Text("$selectedItem screen content")
}
}
}
}
Best Practices
- Use
ModalNavigationDrawerfor phones - Use
PermanentNavigationDraweron tablets - Implement
WindowSizeClassfor responsive layouts - Add profile/account header for context
- Keep drawer items concise and well-organized
- Smooth state transitions with coroutines
8 Android App Templates → https://myougatheax.gumroad.com
Top comments (0)