androidx.compose.material3.adaptive.layout

  • Common/All

Interfaces

AdaptStrategy

Provides the information about how the associated pane should be adapted if it cannot be displayed in its PaneAdaptedValue.Expanded state.

Cmn
AnimatedPaneScope

Scope for the content of AnimatedPane.

Cmn
ExtendedPaneScaffoldPaneScope

Extended scope for the panes of pane scaffolds.

Cmn
ExtendedPaneScaffoldScope

Extended scope for pane scaffolds.

Cmn
PaneExpansionStateKey

Interface that serves as keys to remember and retrieve PaneExpansionState with rememberPaneExpansionState.

Cmn
PaneExpansionStateKeyProvider

Interface that provides PaneExpansionStateKey to remember and retrieve PaneExpansionState with rememberPaneExpansionState.

Cmn
PaneMotion

Interface to specify a custom pane enter/exit motion when a pane's visibility changes.

Cmn
PaneScaffoldMotionDataProvider

Scope for performing pane motions within a pane scaffold.

Cmn
PaneScaffoldPaneScope

The pane scope of the current pane under the scope, which provides the pane relevant info like its role and PaneMotion.

Cmn
PaneScaffoldParentData

The parent data passed to pane scaffolds by their contents like panes and drag handles.

Cmn
PaneScaffoldScope

The base scope of pane scaffolds, which provides scoped functions that supported by pane scaffolds.

Cmn
PaneScaffoldTransitionScope

The transition scope of pane scaffold implementations, which provides the current transition info of the associated pane scaffold.

Cmn
PaneScaffoldValue

Interface to provide adapted value of panes.

Cmn
ThreePaneScaffoldOverride

Interface that allows libraries to override the behavior of ThreePaneScaffold.

Cmn
ThreePaneScaffoldPaneScope

Scope for the panes of ThreePaneScaffold.

Cmn
ThreePaneScaffoldScope

Scope for the panes of ThreePaneScaffold.

Cmn

Classes

HingePolicy

Policies that indicate how hinges are supposed to be addressed in an adaptive layout.

Cmn
MutableThreePaneScaffoldState

The seekable state of a three pane scaffold.

Cmn
PaneAdaptedValue

The adapted state of a pane.

Cmn
PaneExpansionAnchor

The implementations of this interface represent different types of anchors of pane expansion dragging.

Cmn
PaneExpansionAnchor.Offset

PaneExpansionAnchor implementation that specifies the anchor position in the offset in Dp.

Cmn
PaneExpansionAnchor.Proportion

PaneExpansionAnchor implementation that specifies the anchor position in the proportion of the total size of the layout at the start side of the anchor.

Cmn
PaneExpansionState

This class manages the pane expansion state for pane scaffolds.

Cmn
PaneMotion.Type

Indicates the current type of pane motion, like if the pane is entering or exiting, or is kept showing or hidden.

Cmn
PaneMotionData

A class to collect motion-relevant data of a specific pane.

Cmn
PaneScaffoldDirective

Top-level directives about how a pane scaffold should be arranged and spaced, like how many partitions the layout can be split into and what should be the gutter size.

Cmn
ThreePaneMotion

The class that provides motion settings for three pane scaffolds like ListDetailPaneScaffold and SupportingPaneScaffold.

Cmn
ThreePaneScaffoldAdaptStrategies

The adaptation specs of ThreePaneScaffold.

Cmn
ThreePaneScaffoldDestinationItem

An item representing a navigation destination in a ThreePaneScaffold.

Cmn
ThreePaneScaffoldHorizontalOrder

Represents the horizontal order of panes in a ThreePaneScaffold from start to end.

Cmn
ThreePaneScaffoldOverrideContext

Parameters available to ThreePaneScaffold.

Cmn
ThreePaneScaffoldState

A read-only state of a three pane scaffold.

Cmn
ThreePaneScaffoldValue

The adapted value of ThreePaneScaffold.

Cmn

Objects

ListDetailPaneScaffoldDefaults

Provides default values of ListDetailPaneScaffold.

Cmn
ListDetailPaneScaffoldRole

The set of the available pane roles of ListDetailPaneScaffold.

Cmn
PaneMotionDefaults

The default settings of pane motions.

Cmn
SupportingPaneScaffoldDefaults

Provides default values of SupportingPaneScaffold.

Cmn
SupportingPaneScaffoldRole

The set of the available pane roles of SupportingPaneScaffold.

Cmn

Enums

ThreePaneScaffoldRole

The set of the available pane roles of ThreePaneScaffold.

Cmn

Top-level functions summary

Unit
@ExperimentalMaterial3AdaptiveApi
@Composable
ListDetailPaneScaffold(
    directive: PaneScaffoldDirective,
    scaffoldState: ThreePaneScaffoldState,
    listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)?,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.

Cmn
Unit
@ExperimentalMaterial3AdaptiveApi
@Composable
ListDetailPaneScaffold(
    directive: PaneScaffoldDirective,
    value: ThreePaneScaffoldValue,
    listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)?,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.

Cmn
Unit
@ExperimentalMaterial3AdaptiveApi
@Composable
SupportingPaneScaffold(
    directive: PaneScaffoldDirective,
    scaffoldState: ThreePaneScaffoldState,
    mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)?,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical supporting pane layout.

Cmn
Unit
@ExperimentalMaterial3AdaptiveApi
@Composable
SupportingPaneScaffold(
    directive: PaneScaffoldDirective,
    value: ThreePaneScaffoldValue,
    mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)?,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)?,
    paneExpansionState: PaneExpansionState?
)

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical supporting pane layout.

Cmn
PaneScaffoldDirective
@ExperimentalMaterial3AdaptiveApi
calculatePaneScaffoldDirective(
    windowAdaptiveInfo: WindowAdaptiveInfo,
    verticalHingePolicy: HingePolicy
)

Calculates the recommended PaneScaffoldDirective from a given WindowAdaptiveInfo.

Cmn
PaneScaffoldDirective

Calculates the recommended PaneScaffoldDirective from a given WindowAdaptiveInfo.

Cmn
ThreePaneScaffoldValue
@ExperimentalMaterial3AdaptiveApi
calculateThreePaneScaffoldValue(
    maxHorizontalPartitions: Int,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    currentDestination: ThreePaneScaffoldDestinationItem<*>?
)

Calculates the current adapted value of ThreePaneScaffold according to the given maxHorizontalPartitions, adaptStrategies and currentDestination.

Cmn
ThreePaneScaffoldValue
@ExperimentalMaterial3AdaptiveApi
calculateThreePaneScaffoldValue(
    maxHorizontalPartitions: Int,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>
)

Calculates the current adapted value of ThreePaneScaffold according to the given maxHorizontalPartitions, adaptStrategies and destinationHistory.

Cmn
PaneExpansionState
@ExperimentalMaterial3AdaptiveApi
@Composable
rememberPaneExpansionState(
    key: PaneExpansionStateKey,
    anchors: List<PaneExpansionAnchor>,
    initialAnchoredIndex: Int,
    anchoringAnimationSpec: FiniteAnimationSpec<Float>,
    flingBehavior: FlingBehavior
)

Remembers and returns a PaneExpansionState associated to a given PaneExpansionStateKey.

Cmn
PaneExpansionState
@ExperimentalMaterial3AdaptiveApi
@Composable
rememberPaneExpansionState(
    keyProvider: PaneExpansionStateKeyProvider,
    anchors: List<PaneExpansionAnchor>,
    initialAnchoredIndex: Int,
    anchoringAnimationSpec: FiniteAnimationSpec<Float>,
    flingBehavior: FlingBehavior
)

Remembers and returns a PaneExpansionState associated to a given PaneExpansionStateKeyProvider.

Cmn

Extension functions summary

Unit
@ExperimentalMaterial3AdaptiveApi
@Composable
<S : Any?, T : PaneScaffoldValue<S>> ExtendedPaneScaffoldPaneScope<S, T>.AnimatedPane(
    modifier: Modifier,
    enterTransition: EnterTransition,
    exitTransition: ExitTransition,
    boundsAnimationSpec: FiniteAnimationSpec<IntRect>,
    content: @Composable AnimatedPaneScope.() -> Unit
)

The root composable of pane contents in a ThreePaneScaffold that supports default motions during pane switching.

Cmn
inline Unit

Perform actions on each PaneMotionData, in the left-to-right order of the panes in the scaffold.

Cmn
inline Unit

Perform actions on each PaneMotionData, in the right-to-left order of the panes in the scaffold.

Cmn
ThreePaneScaffoldHorizontalOrder

Converts a bidirectional order to a left-to-right order.

Cmn

Top-level properties summary

Top-level functions

ListDetailPaneScaffold

@ExperimentalMaterial3AdaptiveApi
@Composable
fun ListDetailPaneScaffold(
    directive: PaneScaffoldDirective,
    scaffoldState: ThreePaneScaffoldState,
    listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier = Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
    paneExpansionState: PaneExpansionState? = null
): Unit

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.

This overload takes a ThreePaneScaffoldState describing the current ThreePaneScaffoldValue and any pane transitions or animations in progress.

Here's a basic usage sample, which demonstrates how a layout can change from single pane to dual pane under different window configurations:

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Details")
            }
        }
    }
)

For a more sophisticated sample that supports an extra pane and pane expansion functionality that allows users to drag to change layout split, see:

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
            ) {
                Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                    Text("Detail")
                    Row(
                        modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        Surface(
                            onClick = {
                                coroutineScope.launch { scaffoldNavigator.navigateBack() }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Previous")
                            }
                        }
                        VerticalDivider()
                        Surface(
                            onClick = {
                                coroutineScope.launch {
                                    scaffoldNavigator.navigateTo(
                                        ListDetailPaneScaffoldRole.Extra
                                    )
                                }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Next")
                            }
                        }
                    }
                }
            }
        }
    },
    extraPane = {
        AnimatedPane(modifier = Modifier.fillMaxSize()) {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.tertiary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Extra")
            }
        }
    },
    paneExpansionState =
        rememberPaneExpansionState(
            keyProvider = scaffoldNavigator.scaffoldValue,
            anchors = PaneExpansionAnchors
        ),
    paneExpansionDragHandle = { state ->
        val interactionSource = remember { MutableInteractionSource() }
        VerticalDragHandle(
            modifier =
                Modifier.paneExpansionDraggable(
                    state,
                    LocalMinimumInteractiveComponentSize.current,
                    interactionSource
                ),
            interactionSource = interactionSource
        )
    }
)

By default there isn't a drag handle rendered so users aren't able to drag to change the pane split. Providing a drag handle like the above sample shows will enable the functionality. We suggest developers to use the vertical drag handle implementation provided by the Material3 component library here to have default theming/styling support. You can integrate the component as the following sample shows:

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier

val interactionSource = remember { MutableInteractionSource() }
VerticalDragHandle(
    modifier =
        Modifier.paneExpansionDraggable(
            state,
            LocalMinimumInteractiveComponentSize.current,
            interactionSource
        ),
    interactionSource = interactionSource
)

Note that if there's no drag handle, you can still modify paneExpansionState directly to apply pane expansion.

The following code gives a sample of how to integrate with the Compose Navigation library:

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

fun ThreePaneScaffoldNavigator<*>.isListExpanded() =
    scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() =
    scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
val welcomeRoute = "welcome"
val listDetailRoute = "listdetail"
val items = List(15) { "Item $it" }
val loremIpsum =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
        "incididunt ut labore et dolore magna aliqua. Dui nunc mattis enim ut tellus " +
        "elementum sagittis. Nunc sed augue lacus viverra vitae. Sit amet dictum sit amet " +
        "donec. Fringilla urna porttitor rhoncus dolor purus non enim praesent elementum."

@Composable
fun ListCard(
    title: String,
    highlight: Boolean,
    modifier: Modifier = Modifier,
) {
    OutlinedCard(
        colors =
            CardDefaults.outlinedCardColors(
                when {
                    highlight -> MaterialTheme.colorScheme.surfaceVariant
                    else -> MaterialTheme.colorScheme.surface
                }
            ),
        modifier = modifier.heightIn(min = 72.dp).fillMaxWidth(),
    ) {
        Text(
            text = title,
            modifier = Modifier.padding(8.dp),
            style = MaterialTheme.typography.headlineLarge,
        )
    }
}

@Composable
fun DetailScreen(
    title: String,
    details: String,
    backButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Scaffold(
        modifier = modifier,
        topBar = { TopAppBar(title = { Text(title) }, navigationIcon = backButton) },
    ) { paddingValues ->
        Card(
            colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant),
            modifier = Modifier.padding(paddingValues).fillMaxSize(),
        ) {
            Text(
                text = details,
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.bodyMedium,
            )
        }
    }
}

// `navController` handles navigation outside the ListDetailPaneScaffold,
// and `scaffoldNavigator` handles navigation within it. The "content" of
// the scaffold uses String ids, which we pass as a type argument to
// `rememberListDetailPaneScaffoldNavigator`. If you don't need the
// scaffold navigator to be aware of its content, you can pass `Nothing`.
val navController = rememberNavController()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<String>()
val coroutineScope = rememberCoroutineScope()

NavHost(
    navController = navController,
    startDestination = welcomeRoute,
    enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
    exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
    popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
    popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
) {
    composable(welcomeRoute) {
        Scaffold(Modifier.fillMaxSize()) { paddingValues ->
            Box(Modifier.padding(paddingValues).fillMaxSize()) {
                Text(
                    text = "Welcome Screen",
                    modifier = Modifier.align(Alignment.TopCenter).padding(top = 24.dp),
                    style = MaterialTheme.typography.displayMedium,
                )
                Button(
                    onClick = { navController.navigate(listDetailRoute) },
                    modifier = Modifier.align(Alignment.Center),
                ) {
                    Text("Next")
                }
            }
        }
    }
    composable(listDetailRoute) {
        val listScrollState = rememberScrollState()
        val selectedItem = scaffoldNavigator.currentDestination?.contentKey

        // Back behavior can be customized based on the scaffold's layout.
        // In this example, back navigation goes item-by-item when both
        // list and detail panes are expanded. But if only one pane is
        // showing, back navigation goes from detail screen to list-screen.
        val backBehavior =
            if (scaffoldNavigator.isListExpanded() && scaffoldNavigator.isDetailExpanded()) {
                BackNavigationBehavior.PopUntilContentChange
            } else {
                BackNavigationBehavior.PopUntilScaffoldValueChange
            }

        BackHandler(enabled = scaffoldNavigator.canNavigateBack(backBehavior)) {
            coroutineScope.launch { scaffoldNavigator.navigateBack(backBehavior) }
        }

        ListDetailPaneScaffold(
            directive = scaffoldNavigator.scaffoldDirective,
            value = scaffoldNavigator.scaffoldValue,
            listPane = {
                AnimatedPane(Modifier.preferredWidth(240.dp)) {
                    Surface {
                        Column(
                            modifier = Modifier.verticalScroll(listScrollState),
                            verticalArrangement = Arrangement.spacedBy(4.dp),
                        ) {
                            items.forEach { item ->
                                ListCard(
                                    title = item,
                                    highlight =
                                        item == selectedItem &&
                                            scaffoldNavigator.isDetailExpanded(),
                                    modifier =
                                        Modifier.clickable {
                                            if (item != selectedItem) {
                                                coroutineScope.launch {
                                                    scaffoldNavigator.navigateTo(
                                                        pane =
                                                            ListDetailPaneScaffoldRole.Detail,
                                                        contentKey = item,
                                                    )
                                                }
                                            }
                                        }
                                )
                            }
                        }
                    }
                }
            },
            detailPane = {
                AnimatedPane {
                    Crossfade(
                        targetState = selectedItem,
                        label = "Detail Pane",
                    ) { item ->
                        val title = item ?: "No item selected"
                        val details =
                            if (item != null) loremIpsum else "Select an item from the list"

                        DetailScreen(
                            title = title,
                            details = details,
                            backButton = {
                                AnimatedVisibility(
                                    visible = !scaffoldNavigator.isListExpanded()
                                ) {
                                    IconButton(
                                        onClick = {
                                            coroutineScope.launch {
                                                scaffoldNavigator.navigateBack(backBehavior)
                                            }
                                        },
                                        content = {
                                            Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
                                        }
                                    )
                                }
                            }
                        )
                    }
                }
            },
        )
    }
}
Parameters
directive: PaneScaffoldDirective

The top-level directives about how the scaffold should arrange its panes.

scaffoldState: ThreePaneScaffoldState

The current state of the scaffold, containing information about the adapted value of each pane of the scaffold and the transitions/animations in progress.

listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the list pane of the scaffold, which is supposed to hold a list of item summaries that can be selected from, for example, the inbox mail list of a mail app. See ListDetailPaneScaffoldRole.List.

detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the detail pane of the scaffold, which is supposed to hold the detailed info of a selected item, for example, the mail content currently being viewed. See ListDetailPaneScaffoldRole.Detail.

modifier: Modifier = Modifier

Modifier of the scaffold layout.

extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null

the extra pane of the scaffold, which is supposed to hold any supplementary info besides the list and the detail panes, for example, a task list or a mini-calendar view of a mail app. See ListDetailPaneScaffoldRole.Extra.

paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null

the pane expansion drag handle to allow users to drag to change pane expansion state, null by default.

paneExpansionState: PaneExpansionState? = null

the state object of pane expansion; when no value is provided but paneExpansionDragHandle is not null, a default implementation will be created for the drag handle to use.

ListDetailPaneScaffold

@ExperimentalMaterial3AdaptiveApi
@Composable
fun ListDetailPaneScaffold(
    directive: PaneScaffoldDirective,
    value: ThreePaneScaffoldValue,
    listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier = Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
    paneExpansionState: PaneExpansionState? = null
): Unit

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical list-detail layout.

This overload takes a ThreePaneScaffoldValue describing the adapted value of each pane within the scaffold.

Here's a basic usage sample, which demonstrates how a layout can change from single pane to dual pane under different window configurations:

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Details")
            }
        }
    }
)

For a more sophisticated sample that supports an extra pane and pane expansion functionality that allows users to drag to change layout split, see:

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
            ) {
                Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                    Text("Detail")
                    Row(
                        modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        Surface(
                            onClick = {
                                coroutineScope.launch { scaffoldNavigator.navigateBack() }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Previous")
                            }
                        }
                        VerticalDivider()
                        Surface(
                            onClick = {
                                coroutineScope.launch {
                                    scaffoldNavigator.navigateTo(
                                        ListDetailPaneScaffoldRole.Extra
                                    )
                                }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Next")
                            }
                        }
                    }
                }
            }
        }
    },
    extraPane = {
        AnimatedPane(modifier = Modifier.fillMaxSize()) {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.tertiary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Extra")
            }
        }
    },
    paneExpansionState =
        rememberPaneExpansionState(
            keyProvider = scaffoldNavigator.scaffoldValue,
            anchors = PaneExpansionAnchors
        ),
    paneExpansionDragHandle = { state ->
        val interactionSource = remember { MutableInteractionSource() }
        VerticalDragHandle(
            modifier =
                Modifier.paneExpansionDraggable(
                    state,
                    LocalMinimumInteractiveComponentSize.current,
                    interactionSource
                ),
            interactionSource = interactionSource
        )
    }
)

By default there isn't a drag handle rendered so users aren't able to drag to change the pane split. Providing a drag handle like the above sample shows will enable the functionality. We suggest developers to use the vertical drag handle implementation provided by the Material3 component library here to have default theming/styling support. You can integrate the component as the following sample shows:

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier

val interactionSource = remember { MutableInteractionSource() }
VerticalDragHandle(
    modifier =
        Modifier.paneExpansionDraggable(
            state,
            LocalMinimumInteractiveComponentSize.current,
            interactionSource
        ),
    interactionSource = interactionSource
)

Note that if there's no drag handle, you can still modify paneExpansionState directly to apply pane expansion.

The following code gives a sample of how to integrate with the Compose Navigation library:

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

fun ThreePaneScaffoldNavigator<*>.isListExpanded() =
    scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
fun ThreePaneScaffoldNavigator<*>.isDetailExpanded() =
    scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
val welcomeRoute = "welcome"
val listDetailRoute = "listdetail"
val items = List(15) { "Item $it" }
val loremIpsum =
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
        "incididunt ut labore et dolore magna aliqua. Dui nunc mattis enim ut tellus " +
        "elementum sagittis. Nunc sed augue lacus viverra vitae. Sit amet dictum sit amet " +
        "donec. Fringilla urna porttitor rhoncus dolor purus non enim praesent elementum."

@Composable
fun ListCard(
    title: String,
    highlight: Boolean,
    modifier: Modifier = Modifier,
) {
    OutlinedCard(
        colors =
            CardDefaults.outlinedCardColors(
                when {
                    highlight -> MaterialTheme.colorScheme.surfaceVariant
                    else -> MaterialTheme.colorScheme.surface
                }
            ),
        modifier = modifier.heightIn(min = 72.dp).fillMaxWidth(),
    ) {
        Text(
            text = title,
            modifier = Modifier.padding(8.dp),
            style = MaterialTheme.typography.headlineLarge,
        )
    }
}

@Composable
fun DetailScreen(
    title: String,
    details: String,
    backButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Scaffold(
        modifier = modifier,
        topBar = { TopAppBar(title = { Text(title) }, navigationIcon = backButton) },
    ) { paddingValues ->
        Card(
            colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant),
            modifier = Modifier.padding(paddingValues).fillMaxSize(),
        ) {
            Text(
                text = details,
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.bodyMedium,
            )
        }
    }
}

// `navController` handles navigation outside the ListDetailPaneScaffold,
// and `scaffoldNavigator` handles navigation within it. The "content" of
// the scaffold uses String ids, which we pass as a type argument to
// `rememberListDetailPaneScaffoldNavigator`. If you don't need the
// scaffold navigator to be aware of its content, you can pass `Nothing`.
val navController = rememberNavController()
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<String>()
val coroutineScope = rememberCoroutineScope()

NavHost(
    navController = navController,
    startDestination = welcomeRoute,
    enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
    exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
    popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
    popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
) {
    composable(welcomeRoute) {
        Scaffold(Modifier.fillMaxSize()) { paddingValues ->
            Box(Modifier.padding(paddingValues).fillMaxSize()) {
                Text(
                    text = "Welcome Screen",
                    modifier = Modifier.align(Alignment.TopCenter).padding(top = 24.dp),
                    style = MaterialTheme.typography.displayMedium,
                )
                Button(
                    onClick = { navController.navigate(listDetailRoute) },
                    modifier = Modifier.align(Alignment.Center),
                ) {
                    Text("Next")
                }
            }
        }
    }
    composable(listDetailRoute) {
        val listScrollState = rememberScrollState()
        val selectedItem = scaffoldNavigator.currentDestination?.contentKey

        // Back behavior can be customized based on the scaffold's layout.
        // In this example, back navigation goes item-by-item when both
        // list and detail panes are expanded. But if only one pane is
        // showing, back navigation goes from detail screen to list-screen.
        val backBehavior =
            if (scaffoldNavigator.isListExpanded() && scaffoldNavigator.isDetailExpanded()) {
                BackNavigationBehavior.PopUntilContentChange
            } else {
                BackNavigationBehavior.PopUntilScaffoldValueChange
            }

        BackHandler(enabled = scaffoldNavigator.canNavigateBack(backBehavior)) {
            coroutineScope.launch { scaffoldNavigator.navigateBack(backBehavior) }
        }

        ListDetailPaneScaffold(
            directive = scaffoldNavigator.scaffoldDirective,
            value = scaffoldNavigator.scaffoldValue,
            listPane = {
                AnimatedPane(Modifier.preferredWidth(240.dp)) {
                    Surface {
                        Column(
                            modifier = Modifier.verticalScroll(listScrollState),
                            verticalArrangement = Arrangement.spacedBy(4.dp),
                        ) {
                            items.forEach { item ->
                                ListCard(
                                    title = item,
                                    highlight =
                                        item == selectedItem &&
                                            scaffoldNavigator.isDetailExpanded(),
                                    modifier =
                                        Modifier.clickable {
                                            if (item != selectedItem) {
                                                coroutineScope.launch {
                                                    scaffoldNavigator.navigateTo(
                                                        pane =
                                                            ListDetailPaneScaffoldRole.Detail,
                                                        contentKey = item,
                                                    )
                                                }
                                            }
                                        }
                                )
                            }
                        }
                    }
                }
            },
            detailPane = {
                AnimatedPane {
                    Crossfade(
                        targetState = selectedItem,
                        label = "Detail Pane",
                    ) { item ->
                        val title = item ?: "No item selected"
                        val details =
                            if (item != null) loremIpsum else "Select an item from the list"

                        DetailScreen(
                            title = title,
                            details = details,
                            backButton = {
                                AnimatedVisibility(
                                    visible = !scaffoldNavigator.isListExpanded()
                                ) {
                                    IconButton(
                                        onClick = {
                                            coroutineScope.launch {
                                                scaffoldNavigator.navigateBack(backBehavior)
                                            }
                                        },
                                        content = {
                                            Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
                                        }
                                    )
                                }
                            }
                        )
                    }
                }
            },
        )
    }
}
Parameters
directive: PaneScaffoldDirective

The top-level directives about how the scaffold should arrange its panes.

value: ThreePaneScaffoldValue

The current adapted value of the scaffold, which indicates how each pane of the scaffold is adapted.

listPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the list pane of the scaffold, which is supposed to hold a list of item summaries that can be selected from, for example, the inbox mail list of a mail app. See ListDetailPaneScaffoldRole.List.

detailPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the detail pane of the scaffold, which is supposed to hold the detailed info of a selected item, for example, the mail content currently being viewed. See ListDetailPaneScaffoldRole.Detail.

modifier: Modifier = Modifier

Modifier of the scaffold layout.

extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null

the extra pane of the scaffold, which is supposed to hold any supplementary info besides the list and the detail panes, for example, a task list or a mini-calendar view of a mail app. See ListDetailPaneScaffoldRole.Extra.

paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null

the pane expansion drag handle to allow users to drag to change pane expansion state, null by default.

paneExpansionState: PaneExpansionState? = null

the state object of pane expansion; when no value is provided but paneExpansionDragHandle is not null, a default implementation will be created for the drag handle to use.

SupportingPaneScaffold

@ExperimentalMaterial3AdaptiveApi
@Composable
fun SupportingPaneScaffold(
    directive: PaneScaffoldDirective,
    scaffoldState: ThreePaneScaffoldState,
    mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier = Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
    paneExpansionState: PaneExpansionState? = null
): Unit

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical supporting pane layout.

This overload takes a ThreePaneScaffoldState describing the current ThreePaneScaffoldValue and any pane transitions or animations in progress.

Parameters
directive: PaneScaffoldDirective

The top-level directives about how the scaffold should arrange its panes.

scaffoldState: ThreePaneScaffoldState

The current state of the scaffold, containing information about the adapted value of each pane of the scaffold and the transitions/animations in progress.

mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the main pane of the scaffold, which is supposed to hold the major content of an app, for example, the editing screen of a doc app. See SupportingPaneScaffoldRole.Main.

supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the supporting pane of the scaffold, which is supposed to hold the support content of an app, for example, the comment list of a doc app. See SupportingPaneScaffoldRole.Supporting.

modifier: Modifier = Modifier

Modifier of the scaffold layout.

extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null

the extra pane of the scaffold, which is supposed to hold any additional content besides the main and the supporting panes, for example, a styling panel in a doc app. See SupportingPaneScaffoldRole.Extra.

paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null

the pane expansion drag handle to allow users to drag to change pane expansion state, null by default.

paneExpansionState: PaneExpansionState? = null

the state object of pane expansion; when no value is provided but paneExpansionDragHandle is not null, a default implementation will be created for the drag handle to use.

SupportingPaneScaffold

@ExperimentalMaterial3AdaptiveApi
@Composable
fun SupportingPaneScaffold(
    directive: PaneScaffoldDirective,
    value: ThreePaneScaffoldValue,
    mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit,
    modifier: Modifier = Modifier,
    extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null,
    paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null,
    paneExpansionState: PaneExpansionState? = null
): Unit

A three pane layout that follows the Material guidelines, displaying the provided panes in a canonical supporting pane layout.

This overload takes a ThreePaneScaffoldValue describing the adapted value of each pane within the scaffold.

Parameters
directive: PaneScaffoldDirective

The top-level directives about how the scaffold should arrange its panes.

value: ThreePaneScaffoldValue

The current adapted value of the scaffold, which indicates how each pane of the scaffold is adapted.

mainPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the main pane of the scaffold, which is supposed to hold the major content of an app, for example, the editing screen of a doc app. See SupportingPaneScaffoldRole.Main.

supportingPane: @Composable ThreePaneScaffoldPaneScope.() -> Unit

the supporting pane of the scaffold, which is supposed to hold the support content of an app, for example, the comment list of a doc app. See SupportingPaneScaffoldRole.Supporting.

modifier: Modifier = Modifier

Modifier of the scaffold layout.

extraPane: (@Composable ThreePaneScaffoldPaneScope.() -> Unit)? = null

the extra pane of the scaffold, which is supposed to hold any additional content besides the main and the supporting panes, for example, a styling panel in a doc app. See SupportingPaneScaffoldRole.Extra.

paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? = null

the pane expansion drag handle to allow users to drag to change pane expansion state, null by default.

paneExpansionState: PaneExpansionState? = null

the state object of pane expansion; when no value is provided but paneExpansionDragHandle is not null, a default implementation will be created for the drag handle to use.

calculatePaneScaffoldDirective

@ExperimentalMaterial3AdaptiveApi
fun calculatePaneScaffoldDirective(
    windowAdaptiveInfo: WindowAdaptiveInfo,
    verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
): PaneScaffoldDirective

Calculates the recommended PaneScaffoldDirective from a given WindowAdaptiveInfo. Use this method with currentWindowAdaptiveInfo to acquire Material-recommended adaptive layout settings of the current activity window.

See more details on the Material design guideline site (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).

Parameters
windowAdaptiveInfo: WindowAdaptiveInfo

WindowAdaptiveInfo that collects useful information in making layout adaptation decisions like WindowSizeClass.

verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating

HingePolicy that decides how layouts are supposed to address vertical hinges.

Returns
PaneScaffoldDirective

an PaneScaffoldDirective to be used to decide adaptive layout states.

calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth

@ExperimentalMaterial3AdaptiveApi
fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
    windowAdaptiveInfo: WindowAdaptiveInfo,
    verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
): PaneScaffoldDirective

Calculates the recommended PaneScaffoldDirective from a given WindowAdaptiveInfo. Use this method with currentWindowAdaptiveInfo to acquire Material-recommended dense-mode adaptive layout settings of the current activity window. Note that this function results in a dual-pane layout when the WindowWidthSizeClass is WindowWidthSizeClass.MEDIUM, while calculatePaneScaffoldDirective results in a single-pane layout instead. We recommend to use calculatePaneScaffoldDirective, unless you have a strong use case to show two panes on a medium-width window, which can make your layout look too packed.

See more details on the Material design guideline site (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).

Parameters
windowAdaptiveInfo: WindowAdaptiveInfo

WindowAdaptiveInfo that collects useful information in making layout adaptation decisions like WindowSizeClass.

verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating

HingePolicy that decides how layouts are supposed to address vertical hinges.

Returns
PaneScaffoldDirective

an PaneScaffoldDirective to be used to decide adaptive layout states.

calculateThreePaneScaffoldValue

@ExperimentalMaterial3AdaptiveApi
fun calculateThreePaneScaffoldValue(
    maxHorizontalPartitions: Int,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    currentDestination: ThreePaneScaffoldDestinationItem<*>?
): ThreePaneScaffoldValue

Calculates the current adapted value of ThreePaneScaffold according to the given maxHorizontalPartitions, adaptStrategies and currentDestination. The returned value can be used as a unique representation of the current layout structure.

The function will treat the current destination as the highest priority and then adapt the rest panes according to the order of ThreePaneScaffoldRole.Primary, ThreePaneScaffoldRole.Secondary and ThreePaneScaffoldRole.Tertiary. If there are still remaining partitions to put the pane, the pane will be set as PaneAdaptedValue.Expanded, otherwise it will be adapted according to its associated AdaptStrategy.

Parameters
maxHorizontalPartitions: Int

The maximum allowed partitions along the horizontal axis, i.e., how many expanded panes can be shown at the same time.

adaptStrategies: ThreePaneScaffoldAdaptStrategies

The adapt strategies of each pane role that ThreePaneScaffold supports, the default value will be ThreePaneScaffoldDefaults.adaptStrategies.

currentDestination: ThreePaneScaffoldDestinationItem<*>?

The current destination item, which will be treated as having the highest priority, can be null.

calculateThreePaneScaffoldValue

@ExperimentalMaterial3AdaptiveApi
fun calculateThreePaneScaffoldValue(
    maxHorizontalPartitions: Int,
    adaptStrategies: ThreePaneScaffoldAdaptStrategies,
    destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>
): ThreePaneScaffoldValue

Calculates the current adapted value of ThreePaneScaffold according to the given maxHorizontalPartitions, adaptStrategies and destinationHistory. The returned value can be used as a unique representation of the current layout structure.

The function will treat the current focus as the highest priority and then adapt the rest panes according to the order of ThreePaneScaffoldRole.Primary, ThreePaneScaffoldRole.Secondary and ThreePaneScaffoldRole.Tertiary. If there are still remaining partitions to put the pane, the pane will be set as PaneAdaptedValue.Expanded, otherwise it will be adapted according to its associated AdaptStrategy.

Parameters
maxHorizontalPartitions: Int

The maximum allowed partitions along the horizontal axis, i.e., how many expanded panes can be shown at the same time.

adaptStrategies: ThreePaneScaffoldAdaptStrategies

The adapt strategies of each pane role that ThreePaneScaffold supports, the default value will be ThreePaneScaffoldDefaults.adaptStrategies.

destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>

The history of past destination items. The last destination will have the highest priority, and the second last destination will have the second highest priority, and so forth until all panes have a priority assigned. Note that the last destination is supposed to be the last item of the provided list.

rememberPaneExpansionState

@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberPaneExpansionState(
    key: PaneExpansionStateKey = PaneExpansionStateKey.Default,
    anchors: List<PaneExpansionAnchor> = emptyList(),
    initialAnchoredIndex: Int = -1,
    anchoringAnimationSpec: FiniteAnimationSpec<Float> = DefaultAnchoringAnimationSpec,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior()
): PaneExpansionState

Remembers and returns a PaneExpansionState associated to a given PaneExpansionStateKey.

Note that the remembered PaneExpansionState with all keys that have been used will be persistent through the associated pane scaffold's lifecycles.

Parameters
key: PaneExpansionStateKey = PaneExpansionStateKey.Default

the key of PaneExpansionStateKey

anchors: List<PaneExpansionAnchor> = emptyList()

the anchor list of the returned PaneExpansionState

initialAnchoredIndex: Int = -1

the index of the anchor that is supposed to be used during the initial layout of the associated scaffold; it has to be a valid index of the provided anchors otherwise the function throws; by default the value will be -1 and no initial anchor will be used.

anchoringAnimationSpec: FiniteAnimationSpec<Float> = DefaultAnchoringAnimationSpec

the animation spec used to perform anchoring animation; by default it will be a spring motion.

flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior()

the fling behavior used to handle flings; by default ScrollableDefaults.flingBehavior will be applied.

rememberPaneExpansionState

@ExperimentalMaterial3AdaptiveApi
@Composable
fun rememberPaneExpansionState(
    keyProvider: PaneExpansionStateKeyProvider,
    anchors: List<PaneExpansionAnchor> = emptyList(),
    initialAnchoredIndex: Int = -1,
    anchoringAnimationSpec: FiniteAnimationSpec<Float> = DefaultAnchoringAnimationSpec,
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior()
): PaneExpansionState

Remembers and returns a PaneExpansionState associated to a given PaneExpansionStateKeyProvider.

Note that the remembered PaneExpansionState with all keys that have been used will be persistent through the associated pane scaffold's lifecycles.

Parameters
keyProvider: PaneExpansionStateKeyProvider

the provider of PaneExpansionStateKey

anchors: List<PaneExpansionAnchor> = emptyList()

the anchor list of the returned PaneExpansionState

initialAnchoredIndex: Int = -1

the index of the anchor that is supposed to be used during the initial layout of the associated scaffold; it has to be a valid index of the provided anchors otherwise the function throws; by default the value will be -1 and no initial anchor will be used.

anchoringAnimationSpec: FiniteAnimationSpec<Float> = DefaultAnchoringAnimationSpec

the animation spec used to perform anchoring animation; by default it will be a spring motion.

flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior()

the fling behavior used to handle flings; by default ScrollableDefaults.flingBehavior will be applied.

Extension functions

@ExperimentalMaterial3AdaptiveApi
@Composable
fun <S : Any?, T : PaneScaffoldValue<S>> ExtendedPaneScaffoldPaneScope<S, T>.AnimatedPane(
    modifier: Modifier = Modifier,
    enterTransition: EnterTransition = paneMotion.enterTransition,
    exitTransition: ExitTransition = paneMotion.exitTransition,
    boundsAnimationSpec: FiniteAnimationSpec<IntRect> = PaneMotionDefaults.AnimationSpec,
    content: @Composable AnimatedPaneScope.() -> Unit
): Unit

The root composable of pane contents in a ThreePaneScaffold that supports default motions during pane switching. It's recommended to use this composable to wrap your own contents when passing them into pane parameters of the scaffold functions, therefore your panes can have a nice default animation for free.

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Details")
            }
        }
    }
)
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.PaneExpansionState
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()
val coroutineScope = rememberCoroutineScope()
ListDetailPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    value = scaffoldNavigator.scaffoldValue,
    listPane = {
        AnimatedPane(
            modifier = Modifier.preferredWidth(200.dp),
        ) {
            Surface(
                color = MaterialTheme.colorScheme.secondary,
                onClick = {
                    coroutineScope.launch {
                        scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
                    }
                }
            ) {
                Text("List")
            }
        }
    },
    detailPane = {
        AnimatedPane(modifier = Modifier) {
            Surface(
                color = MaterialTheme.colorScheme.primary,
            ) {
                Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                    Text("Detail")
                    Row(
                        modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        Surface(
                            onClick = {
                                coroutineScope.launch { scaffoldNavigator.navigateBack() }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Previous")
                            }
                        }
                        VerticalDivider()
                        Surface(
                            onClick = {
                                coroutineScope.launch {
                                    scaffoldNavigator.navigateTo(
                                        ListDetailPaneScaffoldRole.Extra
                                    )
                                }
                            },
                            modifier = Modifier.weight(0.5f).fillMaxHeight(),
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
                        ) {
                            Box(
                                modifier = Modifier.fillMaxSize(),
                                contentAlignment = Alignment.Center
                            ) {
                                Text("Next")
                            }
                        }
                    }
                }
            }
        }
    },
    extraPane = {
        AnimatedPane(modifier = Modifier.fillMaxSize()) {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.tertiary,
                onClick = { coroutineScope.launch { scaffoldNavigator.navigateBack() } }
            ) {
                Text("Extra")
            }
        }
    },
    paneExpansionState =
        rememberPaneExpansionState(
            keyProvider = scaffoldNavigator.scaffoldValue,
            anchors = PaneExpansionAnchors
        ),
    paneExpansionDragHandle = { state ->
        val interactionSource = remember { MutableInteractionSource() }
        VerticalDragHandle(
            modifier =
                Modifier.paneExpansionDraggable(
                    state,
                    LocalMinimumInteractiveComponentSize.current,
                    interactionSource
                ),
            interactionSource = interactionSource
        )
    }
)
Parameters
modifier: Modifier = Modifier

The modifier applied to the AnimatedPane.

enterTransition: EnterTransition = paneMotion.enterTransition

The EnterTransition used to animate the pane in.

exitTransition: ExitTransition = paneMotion.exitTransition

The ExitTransition used to animate the pane out.

boundsAnimationSpec: FiniteAnimationSpec<IntRect> = PaneMotionDefaults.AnimationSpec

The FiniteAnimationSpec used to animate the bounds of the pane when the pane is keeping showing but changing its size and/or position.

content: @Composable AnimatedPaneScope.() -> Unit

The content of the AnimatedPane. Also see AnimatedPaneScope.

See usage samples at:

@ExperimentalMaterial3AdaptiveApi
inline fun <Role : Any?> PaneScaffoldMotionDataProvider<Role>.forEach(
    action: (Role, PaneMotionData) -> Unit
): Unit

Perform actions on each PaneMotionData, in the left-to-right order of the panes in the scaffold.

Parameters
action: (Role, PaneMotionData) -> Unit

action to perform on each PaneMotionData.

@ExperimentalMaterial3AdaptiveApi
inline fun <Role : Any?> PaneScaffoldMotionDataProvider<Role>.forEachReversed(
    action: (Role, PaneMotionData) -> Unit
): Unit

Perform actions on each PaneMotionData, in the right-to-left order of the panes in the scaffold.

Parameters
action: (Role, PaneMotionData) -> Unit

action to perform on each PaneMotionData.

@ExperimentalMaterial3AdaptiveApi
fun ThreePaneScaffoldHorizontalOrder.toLtrOrder(
    layoutDirection: LayoutDirection
): ThreePaneScaffoldHorizontalOrder

Converts a bidirectional order to a left-to-right order.

Parameters
layoutDirection: LayoutDirection

the current LayoutDirection

Top-level properties

Hierarchy Viewer is a tool built into Android Device Monitor that allows you to measure the layout speed for each view in your layout hierarchy.

Updated May 13, 2024