Build a supporting pane layout

The supporting pane layout keeps the user's focus on the app's main content while displaying relevant supporting information. For example, the main pane might show details about a movie, while the supporting pane lists similar movies, films by the same director, or works featuring the same actors.

For more details, see the Material 3 supporting pane guidelines.

Implement a supporting pane with NavigableSupportingPaneScaffold

NavigableSupportingPaneScaffold is a composable that simplifies implementing a supporting pane layout in Jetpack Compose. It wraps SupportingPaneScaffold and adds built-in navigation and predictive back handling.

A supporting pane scaffold supports up to three panes:

  • Main pane: Displays primary content.
  • Supporting pane: Provides additional context or tools related to the main pane.
  • Extra pane (optional): Used for supplementary content when needed.

The scaffold adapts based on window size:

  • In large windows, the main and supporting panes appear side by side.
  • In small windows, only one pane is visible at a time, switching as users navigate.

    Main content occupying most of the display with supporting content alongside.
    Figure 1. Supporting pane layout.

Add dependencies

NavigableSupportingPaneScaffold is part of the Material 3 adaptive layout library.

Add the following three, related dependencies to the build.gradle file of your app or module:

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

Groovy

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • adaptive: Low-level building blocks such as HingeInfo and Posture
  • adaptive-layout: Adaptive layouts such as ListDetailPaneScaffold and SupportingPaneScaffold
  • adaptive-navigation: Composables for navigating within and between panes, as well as adaptive layouts that support navigation by default such as NavigableListDetailPaneScaffold and NavigableSupportingPaneScaffold

Ensure your project includes compose-material3-adaptive version 1.1.0-beta1 or higher.

Opt-in to the predictive back gesture

To enable predictive back animations in Android 15 or lower, you must opt-in to support the predictive back gesture. To opt-in, add android:enableOnBackInvokedCallback="true" to the <application> [tag or android:enableOnBackInvokedCallback="true" to the <application> tag or individual <activity> tags within your AndroidManifest.xml file.

Once your app targets Android 16 (API level 36) or higher, predictive back is enabled by default.

Create a navigator

In small windows, only one pane displays at a time, so use a ThreePaneScaffoldNavigator to move to and from panes. Create an instance of the navigator with rememberSupportingPaneScaffoldNavigator.

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

Pass the navigator to the scaffold

The scaffold requires a ThreePaneScaffoldNavigator which is an interface representing the state of the scaffold, the ThreePaneScaffoldValue and a PaneScaffoldDirective.

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = { /*...*/ },
    supportingPane = { /*...*/ },
)

The main pane and supporting pane are composables containing your content. Use AnimatedPane to apply the default pane animations during navigation. Use the scaffold value to check whether the supporting pane is hidden; if so, display a button that calls navigateTo(SupportingPaneScaffoldRole.Supporting) to display the supporting pane.

Here's a complete implementation of the scaffold:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        AnimatedPane(
            modifier = Modifier
                .safeContentPadding()
                .background(Color.Red)
        ) {
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden) {
                Button(
                    modifier = Modifier
                        .wrapContentSize(),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting)
                        }
                    }
                ) {
                    Text("Show supporting pane")
                }
            } else {
                Text("Supporting pane is shown")
            }
        }
    },
    supportingPane = {
        AnimatedPane(modifier = Modifier.safeContentPadding()) {
            Text("Supporting pane")
        }
    }
)

Extract pane composables

Extract the individual panes of a SupportingPaneScaffold into their own composables to make them reusable and testable. Use ThreePaneScaffoldScope to access AnimatedPane if you want the default animations:

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.MainPane(
    shouldShowSupportingPaneButton: Boolean,
    onNavigateToSupportingPane: () -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedPane(
        modifier = modifier.safeContentPadding()
    ) {
        // Main pane content
        if (shouldShowSupportingPaneButton) {
            Button(onClick = onNavigateToSupportingPane) {
                Text("Show supporting pane")
            }
        } else {
            Text("Supporting pane is shown")
        }
    }
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.SupportingPane(
    modifier: Modifier = Modifier,
) {
    AnimatedPane(modifier = modifier.safeContentPadding()) {
        // Supporting pane content
        Text("This is the supporting pane")
    }
}

Extracting the panes into composables simplifies the use of the SupportingPaneScaffold (compare the following to the complete implementation of the scaffold in the previous section):

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane() },
)

If you need more control over specific aspects of the scaffold, consider using SupportingPaneScaffold instead of NavigableSupportingPaneScaffold. This accepts a PaneScaffoldDirective and ThreePaneScaffoldValue or ThreePaneScaffoldState separately. This flexibility lets you implement custom logic for pane spacing and determine how many panes should be displayed simultaneously. You can also enable predictive back support by adding ThreePaneScaffoldPredictiveBackHandler.

Add ThreePaneScaffoldPredictiveBackHandler

Attach the predictive back handler that takes a scaffold navigator instance and specify the backBehavior. This determines how destinations are popped from the backstack during back navigation. Then pass the scaffoldDirective and scaffoldState to SupportingPaneScaffold. Use the overload that accepts a ThreePaneScaffoldState, passing in scaffoldNavigator.scaffoldState.

Define the main and supporting panes within SupportingPaneScaffold. Use AnimatedPane for default pane animations.

After you implement these steps, your code should look similar to the following:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

ThreePaneScaffoldPredictiveBackHandler(
    navigator = scaffoldNavigator,
    backBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
)

SupportingPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    scaffoldState = scaffoldNavigator.scaffoldState,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane() },
)