Jetpack Compose Navigation Drawer

The Jetpack Compose navigation drawer provides a convenient way to navigate between different sections of your application. This powerful component slides in from the edge of the screen, typically from the left side, revealing a menu of navigation options. The Jetpack Compose navigation drawer follows Material Design guidelines and can be customized to match your app's theme and user experience requirements.

Before diving into implementation, let's understand the key components involved in creating a Jetpack Compose navigation drawer:

  1. ModalDrawer: The main component that creates the drawer layout in Jetpack Compose
  2. DrawerState: Controls the open/closed state of the Jetpack Compose navigation drawer
  3. rememberDrawerState: Creates and remembers the state of your navigation drawer
  4. Scaffold: A layout structure that implements the basic material design visual layout

Setting Up Jetpack Compose Navigation Drawer

To implement a Jetpack Compose navigation drawer, we need to add the necessary dependencies to our project. Let's start with the required dependencies in your build.gradle file:

dependencies {
    // Core Jetpack Compose dependencies
    implementation "androidx.compose.ui:ui:1.5.4"
    implementation "androidx.compose.material:material:1.5.4"
    implementation "androidx.compose.ui:ui-tooling-preview:1.5.4"
    
    // Navigation Component for Compose
    implementation "androidx.navigation:navigation-compose:2.7.5"
    
    // Optional: Material icons
    implementation "androidx.compose.material:material-icons-extended:1.5.4"
}

Creating a Basic Jetpack Compose Navigation Drawer

Let's start with a basic implementation of a Jetpack Compose navigation drawer. This example will show you how to create a simple drawer with navigation items:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

@Composable
fun BasicNavigationDrawer() {
    // Create a scaffold state to control the drawer
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    
    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text("Jetpack Compose Navigation Drawer") },
                navigationIcon = {
                    IconButton(onClick = {
                        // Open the drawer when the menu icon is clicked
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = "Menu")
                    }
                }
            )
        },
        drawerContent = {
            // Content for the navigation drawer
            Column(
                modifier = Modifier.fillMaxHeight().padding(16.dp)
            ) {
                Text(
                    text = "Navigation Drawer",
                    style = MaterialTheme.typography.h6,
                    modifier = Modifier.padding(bottom = 24.dp)
                )
                
                // Navigation Item: Home
                DrawerItem(
                    icon = Icons.Filled.Home,
                    label = "Home",
                    onClick = {
                        scope.launch {
                            // Close drawer and navigate to Home
                            scaffoldState.drawerState.close()
                            // Navigation logic would go here
                        }
                    }
                )
                
                Spacer(modifier = Modifier.height(16.dp))
                
                // Navigation Item: Settings
                DrawerItem(
                    icon = Icons.Filled.Settings,
                    label = "Settings",
                    onClick = {
                        scope.launch {
                            // Close drawer and navigate to Settings
                            scaffoldState.drawerState.close()
                            // Navigation logic would go here
                        }
                    }
                )
            }
        },
        content = {
            // Main content of your screen
            Box(
                modifier = Modifier.fillMaxSize().padding(it),
                contentAlignment = Alignment.Center
            ) {
                Text("Main Content Area")
            }
        }
    )
}

@Composable
fun DrawerItem(
    icon: ImageVector,
    label: String,
    onClick: () -> Unit
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClick = onClick)
            .padding(vertical = 8.dp)
    ) {
        Icon(
            imageVector = icon,
            contentDescription = label,
            modifier = Modifier.size(24.dp)
        )
        Spacer(modifier = Modifier.width(16.dp))
        Text(
            text = label,
            style = MaterialTheme.typography.body1
        )
    }
}

This basic example demonstrates how to create a simple Jetpack Compose navigation drawer with menu items. Let's explore some of the key properties and customizations you can apply to enhance your Jetpack Compose navigation drawer.

Customizing the Jetpack Compose Navigation Drawer

The Jetpack Compose navigation drawer can be customized in many ways to match your app's design requirements. Here are some customization options:

Drawer Width

You can adjust the width of your Jetpack Compose navigation drawer:

drawerContent = {
    Column(
        modifier = Modifier
            .fillMaxHeight()
            .width(300.dp)  // Custom drawer width
            .padding(16.dp)
    ) {
        // Drawer content
    }
}

Drawer Background Color

Customize the background color of your Jetpack Compose navigation drawer:

drawerContent = {
    Surface(
        color = MaterialTheme.colors.surface,  // Or any custom color
        modifier = Modifier.fillMaxHeight()
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            // Drawer content
        }
    }
}

Drawer Shape

You can also customize the shape of your Jetpack Compose navigation drawer:

Scaffold(
    drawerShape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
    // other properties
)

Adding Navigation to the Jetpack Compose Navigation Drawer

A Jetpack Compose navigation drawer is most useful when integrated with the Navigation Component. Let's implement a navigation drawer with actual screen navigation:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch

@Composable
fun NavigationDrawerWithNavigation() {
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    val navController = rememberNavController()
    
    // Track the current screen to update the top bar title
    var currentScreen by remember { mutableStateOf("Home") }
    
    Scaffold(
        scaffoldState = scaffoldState,
        topBar = {
            TopAppBar(
                title = { Text(currentScreen) },
                navigationIcon = {
                    IconButton(onClick = {
                        scope.launch {
                            scaffoldState.drawerState.open()
                        }
                    }) {
                        Icon(Icons.Filled.Menu, contentDescription = "Menu")
                    }
                }
            )
        },
        drawerContent = {
            DrawerHeader()
            DrawerBody(
                navController = navController,
                scaffoldState = scaffoldState,
                scope = scope,
                onDestinationChanged = { screen ->
                    currentScreen = screen
                }
            )
        },
        content = { paddingValues ->
            NavHost(
                navController = navController,
                startDestination = "home",
                modifier = Modifier.padding(paddingValues)
            ) {
                composable("home") {
                    HomeScreen()
                }
                composable("profile") {
                    ProfileScreen()
                }
                composable("settings") {
                    SettingsScreen()
                }
                composable("help") {
                    HelpScreen()
                }
            }
        }
    )
}

@Composable
fun DrawerHeader() {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(180.dp)
            .padding(vertical = 16.dp),
        contentAlignment = Alignment.Center
    ) {
        Column(horizontalAlignment = Alignment.CenterHorizontally) {
            // You can add an app logo or user profile image here
            Surface(
                modifier = Modifier.size(80.dp),
                shape = MaterialTheme.shapes.circle,
                color = MaterialTheme.colors.primary
            ) {
                // Placeholder for logo/profile image
            }
            
            Spacer(modifier = Modifier.height(16.dp))
            
            Text(
                text = "My Compose App",
                style = MaterialTheme.typography.h6
            )
            
            Text(
                text = "example@email.com",
                style = MaterialTheme.typography.body2
            )
        }
    }
    
    Divider()
}

@Composable
fun DrawerBody(
    navController: NavController,
    scaffoldState: ScaffoldState,
    scope: CoroutineScope,
    onDestinationChanged: (String) -> Unit
) {
    val menuItems = listOf(
        MenuItem(
            title = "Home",
            icon = Icons.Default.Home,
            route = "home"
        ),
        MenuItem(
            title = "Profile",
            icon = Icons.Default.Person,
            route = "profile"
        ),
        MenuItem(
            title = "Settings",
            icon = Icons.Default.Settings,
            route = "settings"
        ),
        MenuItem(
            title = "Help",
            icon = Icons.Default.Info,
            route = "help"
        )
    )
    
    Column(modifier = Modifier.padding(16.dp)) {
        menuItems.forEach { item ->
            DrawerItem(
                icon = item.icon,
                label = item.title,
                onClick = {
                    navController.navigate(item.route) {
                        // Pop up to the start destination of the graph to
                        // avoid building up a large stack of destinations
                        popUpTo(navController.graph.startDestinationId)
                        // Avoid multiple copies of the same destination when
                        // reselecting the same item
                        launchSingleTop = true
                    }
                    
                    // Update the current screen title
                    onDestinationChanged(item.title)
                    
                    // Close the drawer
                    scope.launch {
                        scaffoldState.drawerState.close()
                    }
                }
            )
            
            Spacer(modifier = Modifier.height(16.dp))
        }
    }
}

// Data class for menu items
data class MenuItem(
    val title: String,
    val icon: ImageVector,
    val route: String
)

// Example screen composables
@Composable
fun HomeScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Home Screen Content")
    }
}

@Composable
fun ProfileScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Profile Screen Content")
    }
}

@Composable
fun SettingsScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Settings Screen Content")
    }
}

@Composable
fun HelpScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Help Screen Content")
    }
}

Handling Drawer Gestures

The Jetpack Compose navigation drawer supports gesture-based interactions. Users can swipe from the edge of the screen to open the drawer or swipe the drawer to close it. The drawerGesturesEnabled property allows you to control this behavior:

Scaffold(
    scaffoldState = scaffoldState,
    drawerGesturesEnabled = true,  // Enable or disable drawer gestures
    // other properties
)

Persistent vs. Modal Navigation Drawer

Jetpack Compose offers two types of navigation drawers:

  1. ModalDrawer: A temporary drawer that overlays the content and can be dismissed by tapping outside or swiping it away.
  2. PermanentDrawer: A persistent drawer that's always visible and doesn't overlay the content.

Here's how you can implement a permanent navigation drawer for larger screens:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun PermanentNavigationDrawerExample() {
    Row {
        // Permanent navigation drawer - always visible
        Surface(
            modifier = Modifier
                .width(240.dp)
                .fillMaxHeight(),
            elevation = 4.dp
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = "Permanent Drawer",
                    style = MaterialTheme.typography.h6,
                    modifier = Modifier.padding(bottom = 24.dp)
                )
                
                // Drawer items go here
            }
        }
        
        // Main content
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
            Text("Main Content")
        }
    }
}

Implementing a Responsive Navigation Drawer

For a responsive design, you might want to show a permanent drawer on larger screens and a modal drawer on smaller screens:

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

@Composable
fun ResponsiveNavigationDrawer() {
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    val configuration = LocalConfiguration.current
    
    // Determine if we should use permanent or modal drawer based on screen width
    val isExpandedScreen = configuration.screenWidthDp >= 600
    
    if (isExpandedScreen) {
        // Use permanent drawer for larger screens
        Row {
            Surface(
                modifier = Modifier
                    .width(240.dp)
                    .fillMaxHeight(),
                elevation = 4.dp
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = "Navigation Drawer",
                        style = MaterialTheme.typography.h6,
                        modifier = Modifier.padding(bottom = 24.dp)
                    )
                    // Drawer items
                }
            }
            
            // Main content
            Box(modifier = Modifier.fillMaxSize()) {
                Text("Main Content")
            }
        }
    } else {
        // Use modal drawer for smaller screens
        Scaffold(
            scaffoldState = scaffoldState,
            topBar = {
                TopAppBar(
                    title = { Text("Jetpack Compose Navigation Drawer") },
                    navigationIcon = {
                        IconButton(onClick = {
                            scope.launch {
                                scaffoldState.drawerState.open()
                            }
                        }) {
                            Icon(Icons.Filled.Menu, contentDescription = "Menu")
                        }
                    }
                )
            },
            drawerContent = {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = "Navigation Drawer",
                        style = MaterialTheme.typography.h6,
                        modifier = Modifier.padding(bottom = 24.dp)
                    )
                    // Drawer items
                }
            }
        ) { paddingValues ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues)
            ) {
                Text("Main Content")
            }
        }
    }
}

Complete Working Example of Jetpack Compose Navigation Drawer

Let's put everything together into a complete, working example that you can use in your Android app:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNavigationDrawerTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    ComposeNavigationDrawerApp()
                }
            }
        }
    }
}

@Composable
fun ComposeNavigationDrawerTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colors = lightColors(
            primary = Color(0xFF6200EE),
            primaryVariant = Color(0xFF3700B3),
            secondary = Color(0xFF03DAC6)
        ),
        content = content
    )
}

@Composable
fun ComposeNavigationDrawerApp() {
    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    val navController = rememberNavController()
    val configuration = LocalConfiguration.current
    
    // Determine if we should use permanent or modal drawer based on screen width
    val isExpandedScreen = configuration.screenWidthDp >= 600
    
    // Navigation items
    val navItems = listOf(
        NavItem("Home", Icons.Default.Home, "home"),
        NavItem("Profile", Icons.Default.Person, "profile"),
        NavItem("Settings", Icons.Default.Settings, "settings"),
        NavItem("Favorites", Icons.Default.Favorite, "favorites"),
        NavItem("Help", Icons.Default.Info, "help")
    )
    
    // Get current route to highlight the active item
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    
    if (isExpandedScreen) {
        // Permanent drawer layout for larger screens
        Row(modifier = Modifier.fillMaxSize()) {
            PermanentDrawerContent(
                navItems = navItems,
                currentRoute = currentRoute,
                onNavItemClick = { route ->
                    navigateToScreen(navController, route)
                }
            )
            
            Box(modifier = Modifier.weight(1f)) {
                NavigationContent(navController = navController)
            }
        }
    } else {
        // Modal drawer layout for smaller screens
        Scaffold(
            scaffoldState = scaffoldState,
            topBar = {
                TopAppBar(
                    title = { 
                        Text(
                            text = navItems.find { it.route == currentRoute }?.title ?: "Compose App",
                        )
                    },
                    navigationIcon = {
                        IconButton(onClick = {
                            scope.launch {
                                scaffoldState.drawerState.open()
                            }
                        }) {
                            Icon(Icons.Default.Menu, contentDescription = "Menu")
                        }
                    }
                )
            },
            drawerContent = {
                ModalDrawerContent(
                    navItems = navItems,
                    currentRoute = currentRoute,
                    onNavItemClick = { route ->
                        navigateToScreen(navController, route)
                        scope.launch {
                            scaffoldState.drawerState.close()
                        }
                    }
                )
            }
        ) { paddingValues ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues)
            ) {
                NavigationContent(navController = navController)
            }
        }
    }
}

@Composable
fun PermanentDrawerContent(
    navItems: List<NavItem>,
    currentRoute: String?,
    onNavItemClick: (String) -> Unit
) {
    Surface(
        modifier = Modifier
            .width(240.dp)
            .fillMaxHeight(),
        elevation = 4.dp
    ) {
        Column(
            modifier = Modifier.padding(vertical = 24.dp)
        ) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = "Navigation Drawer",
                    style = MaterialTheme.typography.h6
                )
            }
            
            Spacer(modifier = Modifier.height(32.dp))
            
            navItems.forEach { item ->
                val isSelected = currentRoute == item.route
                
                Surface(
                    color = if (isSelected) MaterialTheme.colors.primary.copy(alpha = 0.12f) else Color.Transparent,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable { onNavItemClick(item.route) }
                            .padding(horizontal = 16.dp, vertical = 12.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Icon(
                            imageVector = item.icon,
                            contentDescription = item.title,
                            tint = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
                        )
                        
                        Spacer(modifier = Modifier.width(16.dp))
                        
                        Text(
                            text = item.title,
                            style = MaterialTheme.typography.body1,
                            color = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
                        )
                    }
                }
                
                Spacer(modifier = Modifier.height(4.dp))
            }
        }
    }
}

@Composable
fun ModalDrawerContent(
    navItems: List<NavItem>,
    currentRoute: String?,
    onNavItemClick: (String) -> Unit
) {
    Column(
        modifier = Modifier.fillMaxHeight()
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(180.dp),
            contentAlignment = Alignment.Center
        ) {
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Surface(
                    modifier = Modifier.size(72.dp),
                    shape = MaterialTheme.shapes.circle,
                    color = MaterialTheme.colors.primary
                ) {
                    // App logo or user avatar
                }
                
                Spacer(modifier = Modifier.height(16.dp))
                
                Text(
                    text = "Compose Drawer App",
                    style = MaterialTheme.typography.h6
                )
                
                Text(
                    text = "example@email.com",
                    style = MaterialTheme.typography.body2,
                    color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
                )
            }
        }
        
        Divider()
        
        Spacer(modifier = Modifier.height(16.dp))
        
        navItems.forEach { item ->
            val isSelected = currentRoute == item.route
            
            Surface(
                color = if (isSelected) MaterialTheme.colors.primary.copy(alpha = 0.12f) else Color.Transparent,
                modifier = Modifier.fillMaxWidth()
            ) {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable { onNavItemClick(item.route) }
                        .padding(horizontal = 16.dp, vertical = 12.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Icon(
                        imageVector = item.icon,
                        contentDescription = item.title,
                        tint = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
                    )
                    
                    Spacer(modifier = Modifier.width(16.dp))
                    
                    Text(
                        text = item.title,
                        style = MaterialTheme.typography.body1,
                        color = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
                    )
                }
            }
            
            Spacer(modifier = Modifier.height(4.dp))
        }
    }
}

@Composable
fun NavigationContent(navController: NavController) {
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            ScreenContent(
                title = "Home Screen",
                backgroundColor = Color(0xFFE3F2FD)
            )
        }
        composable("profile") {
            ScreenContent(
                title = "Profile Screen",
                backgroundColor = Color(0xFFE8F5E9)
            )
        }
        composable("settings") {
            ScreenContent(
                title = "Settings Screen",
                backgroundColor = Color(0xFFFFF3E0)
            )
        }
        composable("favorites") {
            ScreenContent(
                title = "Favorites Screen",
                backgroundColor = Color(0xFFF3E5F5)
            )
        }
        composable("help") {
            ScreenContent(
                title = "Help Screen",
                backgroundColor = Color(0xFFEFEBE9)
            )
        }
    }
}

@Composable
fun ScreenContent(
    title: String,
    backgroundColor: Color
) {
    Surface(
        modifier = Modifier.fillMaxSize(),
        color = backgroundColor
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = title,
                style = MaterialTheme.typography.h4,
                textAlign = TextAlign.Center
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            Text(
                text = "This is the content for the $title",
                style = MaterialTheme.typography.body1,
                textAlign = TextAlign.Center,
                modifier = Modifier.padding(horizontal = 32.dp)
            )
        }
    }
}

data class NavItem(
    val title: String,
    val icon: ImageVector,
    val route: String
)

private fun navigateToScreen(navController: NavController, route: String) {
    navController.navigate(route) {
        // Pop up to start destination to avoid building up a large stack
        popUpTo(navController.graph.startDestinationId)
        // Avoid multiple copies of the same destination
        launchSingleTop = true
    }
}