When to Use Navigation Drawers and Tabs

Use Navigation Drawers When:

  • You have many main sections in your app (5+ items)
  • You want to save screen space for content
  • You need to access settings, profile, or help sections
  • You want to provide quick access to all app features
  • You're building a complex app with multiple main areas

Use Tabs When:

  • You have 2-5 related content sections
  • Users need to switch between different views frequently
  • You want to show related content side by side
  • You're organizing content by categories or types
  • You want to keep navigation visible and accessible

Common Components and Options

Component What It Does When to Use It
DrawerState Manages the open/closed state of the drawer When you need to control drawer programmatically
TabRow Creates a horizontal row of tabs For top-level navigation between sections
Tab Individual tab in a tab row For each section or category in your app
TabPosition Defines the position and styling of tabs When you need custom tab appearance

Practical Examples

Basic Navigation Drawer
@Composable
fun MyAppWithDrawer() {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    
    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            ModalDrawerSheet {
                Spacer(modifier = Modifier.height(12.dp))
                Text(
                    "My App",
                    modifier = Modifier.padding(16.dp),
                    style = MaterialTheme.typography.titleMedium
                )
                Spacer(modifier = Modifier.height(12.dp))
                
                NavigationDrawerItem(
                    icon = { Icon(Icons.Default.Home, contentDescription = null) },
                    label = { Text("Home") },
                    selected = false,
                    onClick = { /* Navigate to home */ }
                )
                
                NavigationDrawerItem(
                    icon = { Icon(Icons.Default.Person, contentDescription = null) },
                    label = { Text("Profile") },
                    selected = false,
                    onClick = { /* Navigate to profile */ }
                )
                
                NavigationDrawerItem(
                    icon = { Icon(Icons.Default.Settings, contentDescription = null) },
                    label = { Text("Settings") },
                    selected = false,
                    onClick = { /* Navigate to settings */ }
                )
            }
        }
    ) {
        // Your main app content here
        Column {
            TopAppBar(
                title = { Text("My App") },
                navigationIcon = {
                    IconButton(onClick = { scope.launch { drawerState.open() } }) {
                        Icon(Icons.Default.Menu, contentDescription = "Menu")
                    }
                }
            )
            // Rest of your content
        }
    }
}

This example builds a basic app screen with a navigation drawer. When the user taps the menu icon in the top bar, the drawer slides in from the left and shows Home, Profile, and Settings.

How this code works (step by step)
  1. Create drawer state: rememberDrawerState(DrawerValue.Closed) stores whether the drawer is open or closed.
  2. Create a coroutine scope: rememberCoroutineScope() is needed because opening the drawer uses a suspend function.
  3. Wrap screen in ModalNavigationDrawer: this gives the screen drawer behavior.
  4. Define drawerContent: ModalDrawerSheet contains the title and NavigationDrawerItem entries.
  5. Add menu button: the TopAppBar navigation icon calls drawerState.open() inside scope.launch { ... }.

Think of drawer state like a light switch: it is either ON (open) or OFF (closed). UI state variables like this are the foundation of Compose programming.

Top Tabs with Content
@Composable
fun TabbedContent() {
    var selectedTabIndex by remember { mutableStateOf(0) }
    val tabs = listOf("Recent", "Popular", "Favorites")
    
    Column {
        TabRow(selectedTabIndex = selectedTabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(
                    selected = selectedTabIndex == index,
                    onClick = { selectedTabIndex = index },
                    text = { Text(title) }
                )
            }
        }
        
        when (selectedTabIndex) {
            0 -> RecentContent()
            1 -> PopularContent()
            2 -> FavoritesContent()
        }
    }
}

@Composable
fun RecentContent() {
    LazyColumn {
        items(10) { index ->
            ListItem(
                headlineContent = { Text("Recent Item ${index + 1}") },
                supportingContent = { Text("This is a recent item") }
            )
        }
    }
}

This example creates a top tab menu with three choices: Recent, Popular, and Favorites. Tapping a tab updates a state variable, and the screen shows the matching content section.

How this code works (step by step)
  1. Track selected tab: selectedTabIndex starts at 0, so the first tab is selected at launch.
  2. Store tab labels: a list holds the names so tabs can be built in a loop.
  3. Build tabs in TabRow: forEachIndexed creates one Tab per label.
  4. Handle clicks: onClick = { selectedTabIndex = index } changes state when the user taps a tab.
  5. Render content with when: the when (selectedTabIndex) block decides which composable to display.

In Compose, changing state triggers recomposition (UI redraw). You do not manually refresh the screen.

Scrollable Tab Row
@Composable
fun ScrollableTabs() {
    var selectedTabIndex by remember { mutableStateOf(0) }
    val categories = listOf(
        "All", "Technology", "Science", "Sports", "Entertainment", 
        "Politics", "Business", "Health", "Education", "Travel"
    )
    
    Column {
        ScrollableTabRow(
            selectedTabIndex = selectedTabIndex,
            edgePadding = 16.dp
        ) {
            categories.forEachIndexed { index, category ->
                Tab(
                    selected = selectedTabIndex == index,
                    onClick = { selectedTabIndex = index },
                    text = { Text(category) }
                )
            }
        }
        
        // Content based on selected tab
        when (selectedTabIndex) {
            0 -> AllContent()
            1 -> TechnologyContent()
            // ... other content
        }
    }
}

This example uses ScrollableTabRow because there are many categories. If all tabs cannot fit on one line, the user can scroll sideways to see more.

How this code works (step by step)
  1. Keep tab state: selectedTabIndex still controls which category is active.
  2. Create a longer category list: labels like Technology, Science, Sports, and more are stored in one list.
  3. Use ScrollableTabRow: this is the key difference from TabRow; it supports horizontal scrolling.
  4. Set edge padding: edgePadding = 16.dp adds visual breathing room at both ends.
  5. Switch content by selected index: a when block shows the right content composable.

Use TabRow for a few tabs and ScrollableTabRow when the number of tabs grows.

Combining Drawer and Tabs

The sample drawerTab.kt uses a second piece of state—mainSection—so drawer destinations are not stubs: Home and Settings show full-screen pages (HomeContent, SettingsContent), while News, Sports & Weather returns to the tab strip. The tab row is only composed when mainSection is tabs; choosing Home or Settings hides the tabs and shows that screen instead.

private enum class MainSection { Tabs, Home, Settings }

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppWithDrawerAndTabs() {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    var selectedTab by remember { mutableStateOf(0) }
    var mainSection by remember { mutableStateOf(MainSection.Tabs) }
    val tabs = listOf("News", "Sports", "Weather")

    fun closeDrawer() {
        scope.launch { drawerState.close() }
    }

    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            ModalDrawerSheet {
                Text("News App", modifier = Modifier.padding(16.dp))
                NavigationDrawerItem(
                    icon = { Icon(Icons.Filled.List, contentDescription = null) },
                    label = { Text("News, Sports & Weather") },
                    selected = mainSection == MainSection.Tabs,
                    onClick = {
                        mainSection = MainSection.Tabs
                        closeDrawer()
                    }
                )
                NavigationDrawerItem(
                    icon = { Icon(Icons.Filled.Home, contentDescription = "Home") },
                    label = { Text("Home") },
                    selected = mainSection == MainSection.Home,
                    onClick = {
                        mainSection = MainSection.Home
                        closeDrawer()
                    }
                )
                NavigationDrawerItem(
                    icon = { Icon(Icons.Filled.Settings, contentDescription = "Settings") },
                    label = { Text("Settings") },
                    selected = mainSection == MainSection.Settings,
                    onClick = {
                        mainSection = MainSection.Settings
                        closeDrawer()
                    }
                )
            }
        }
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text(
                            when (mainSection) {
                                MainSection.Home -> "Home"
                                MainSection.Settings -> "Settings"
                                MainSection.Tabs -> "News App"
                            }
                        )
                    },
                    navigationIcon = {
                        IconButton(onClick = { scope.launch { drawerState.open() } }) {
                            Icon(Icons.Filled.Menu, contentDescription = "Menu")
                        }
                    }
                )
            }
        ) { padding ->
            Column(modifier = Modifier.padding(padding)) {
                if (mainSection == MainSection.Tabs) {
                    TabRow(selectedTabIndex = selectedTab) {
                        tabs.forEachIndexed { index, title ->
                            Tab(
                                selected = selectedTab == index,
                                onClick = { selectedTab = index },
                                text = { Text(title) }
                            )
                        }
                    }
                    when (selectedTab) {
                        0 -> NewsContent()
                        1 -> SportsContent()
                        2 -> WeatherContent()
                    }
                } else if (mainSection == MainSection.Home) {
                    HomeContent()
                } else {
                    SettingsContent()
                }
            }
        }
    }
}

HomeContent and SettingsContent are separate composables (same idea as NewsContent—a column with a headline and body text). The drawer’s selected parameter is tied to mainSection so the active destination is highlighted.

How this code works (step by step)
  1. Three layers of state: drawerState (open/closed), selectedTab (which of News/Sports/Weather), and mainSection (tabs vs Home vs Settings).
  2. Drawer items do real navigation: each onClick sets mainSection, then closeDrawer() so the sheet dismisses after a choice.
  3. Return to tabs: the first item (“News, Sports & Weather”) sets mainSection = MainSection.Tabs so the user can get back to the tab row from Home or Settings.
  4. App bar title follows mode: when (mainSection) shows “News App”, “Home”, or “Settings”.
  5. Conditional body: if mainSection == Tabs, show TabRow plus when (selectedTab) { … }; otherwise show HomeContent() or SettingsContent().

Rule of thumb: Drawer = which top-level area (tabs vs Home vs Settings); tabs = which feed while you stay in the tabbed area.