Adaptive NavigationSuiteScaffold for Phone and Tablet

Quick answer: Put your top-level destinations in
NavigationSuiteScaffold. It automatically presents a bottomNavigationBaron compact windows and aNavigationRailwhen the window has more room, while your item definitions and destination state stay the same.
The important input is the app window, not a device label. A tablet in split screen can be compact; a resizable phone window can become wide. NavigationSuiteScaffold selects the navigation control from adaptive window information and updates when the window changes. The official adaptive-navigation guide describes the default as a navigation bar when either dimension is compact or the device is in tabletop posture, and a navigation rail otherwise.
Add the adaptive navigation-suite dependency
NavigationSuiteScaffold is not part of the basic Material 3 navigation-bar artifact. Add the Material 3 adaptive navigation-suite library to the module that renders your app shell:
dependencies {
implementation("androidx.compose.material3:material3-adaptive-navigation-suite")
}Use the version managed by your project’s version catalog or dependency policy. The adaptive navigation-suite API is marked experimental, so opt in at the smallest sensible scope and check the AndroidX release notes when upgrading.
Define destinations once
Keep top-level destinations as data, not as separate bottom-bar and rail implementations. The same item() declaration is rendered as the appropriate Material navigation item for the active window.
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.vector.ImageVector
private enum class AppDestination(
val label: String,
val icon: ImageVector,
) {
Home("Home", Icons.Default.Home),
Profile("Profile", Icons.Default.Person),
Settings("Settings", Icons.Default.Settings),
}
@OptIn(ExperimentalMaterial3AdaptiveNavigationSuiteApi::class)
@Composable
fun AdaptiveAppShell() {
var currentDestination by rememberSaveable {
mutableStateOf(AppDestination.Home)
}
NavigationSuiteScaffold(
navigationSuiteItems = {
AppDestination.entries.forEach { destination ->
item(
selected = destination == currentDestination,
onClick = { currentDestination = destination },
icon = {
Icon(
imageVector = destination.icon,
contentDescription = destination.label,
)
},
label = { Text(destination.label) },
)
}
},
) {
when (currentDestination) {
AppDestination.Home -> HomeScreen()
AppDestination.Profile -> ProfileScreen()
AppDestination.Settings -> SettingsScreen()
}
}
}The sample uses local rememberSaveable state to make the scaffold’s job clear. In a Navigation Compose app, derive selected from the current back-stack destination and navigate from onClick; the surrounding scaffold does not replace NavHost or decide your back-stack policy. Navigation Compose: Create Your First NavHost covers that connection.
What changes at runtime
| Window situation | Default navigation UI | What remains unchanged |
|---|---|---|
| Compact phone or narrow split-screen window | Bottom navigation bar | Destination list, selected state, and click behavior |
| Wider tablet, desktop, or expanded window | Navigation rail | Destination list, selected state, and click behavior |
| Resize, rotation, folding posture change | The suite reevaluates its UI | The selected destination and app data |
This is why keeping the navigation model outside the visual component matters. Do not maintain one selectedTab for a bottom bar and another for a rail: the scaffold may switch controls while the app is running.
Connect it to Navigation Compose
For an app with a NavHost, use one description of your top-level destinations and make the current graph hierarchy determine selection. The navigation action can use the same save-and-restore options as a regular bottom bar:
NavigationSuiteScaffold(
navigationSuiteItems = {
topLevelDestinations.forEach { destination ->
item(
selected = currentDestination?.hierarchy?.any {
it.route == destination.route
} == true,
onClick = {
navController.navigate(destination.route) {
launchSingleTop = true
restoreState = true
popUpTo(
navController.graph.findStartDestination().id,
) {
saveState = true
}
}
},
icon = { Icon(destination.icon, destination.label) },
label = { Text(destination.label) },
)
}
},
) {
AppNavHost(navController = navController)
}The code assumes the usual currentBackStackEntryAsState(), NavDestination.hierarchy, and findStartDestination() setup. The Bottom Navigation with Multiple Back Stacks article explains why saveState, restoreState, and launchSingleTop belong together for top-level destinations.
Customize only for a product reason
The default bar-or-rail behavior covers many apps. Override the suite type only when the content and task call for it—for example, a wide single-pane feed that benefits from a persistent navigation drawer. Use adaptive window information rather than hard-coding a device category:
val adaptiveInfo = currentWindowAdaptiveInfo()
val layoutType = with(adaptiveInfo) {
if (windowSizeClass.isWidthAtLeastBreakpoint(
WIDTH_DP_EXPANDED_LOWER_BOUND,
)
) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(adaptiveInfo)
}
}
NavigationSuiteScaffold(
navigationSuiteItems = { /* items */ },
layoutType = layoutType,
) {
AppContent()
}This override is intentionally more specific than the default and needs the corresponding adaptive imports. It should not be the starting point for every tablet: a rail often leaves more useful room for the content pane. The official guide’s custom navigation-type example has the current API imports and breakpoints.
Insets, accessibility, and testing
NavigationSuiteScaffold provides its content area, but your screen still owns its content layout. Keep the normal Material 3 and Scaffold inset discipline for any nested app bars, lists, or sheets. Scaffold and Window Insets in Jetpack Compose explains how to avoid content hidden by system or app bars.
- Use visible labels for top-level destinations; do not rely on an unfamiliar icon alone.
- Give icon-only variants meaningful content descriptions. When a text label already names the item, avoid duplicating an announcement unnecessarily.
- Test compact, medium, and expanded window sizes, plus split-screen and rotation. Test an actual foldable posture if the app supports one.
- Confirm that selecting a destination, its back stack, and any unsaved user work survive an in-place navigation-control change.
Common mistakes
Deciding from “phone” versus “tablet”
Physical device type is an unreliable proxy for available space. Use the adaptive defaults or current window information so multi-window and resizable layouts behave sensibly.
Duplicating navigation definitions
Two copies of destination labels, icons, and click logic drift over time. Define items once in NavigationSuiteScaffold; it chooses the bar or rail presentation.
Assuming navigation UI makes content adaptive
Switching a bar to a rail changes the navigation area, not a single-pane feed into a list-detail layout. Evaluate the content separately and use adaptive pane scaffolds where the user’s task benefits from simultaneous information.
Removing the experimental opt-in
The annotation is a compatibility signal, not noise. Keep the opt-in localized, track the library release notes, and retest window transitions after updating the dependency.
FAQ
Does NavigationSuiteScaffold automatically create a NavController?
No. It renders navigation controls and destination content. Your app still owns the navigation state, whether that is a simple selected value or Navigation Compose’s NavController and NavHost.
Does it always use a rail on tablets?
No. The decision reacts to the available window and posture. A compact tablet window can receive a bottom bar, while a wider window can receive a rail.
Can I hide the navigation suite on a detail screen?
Yes. The scaffold has a hoistable state that can animate the suite’s visibility. Treat this as a deliberate navigation design choice and test Back, deep links, and accessibility focus when it changes.