Material Components and layouts

Jetpack Compose offers an implementation of Material Design, a comprehensive design system for creating digital interfaces. Material Components (buttons, cards, switches, etc.) and layouts like Scaffold are available as composable functions.

Material Components are interactive building blocks for creating a user interface. Compose offers a number of these components out-of-the-box. To see which are available, check out the Compose Material API reference.

Material Components use values provided by a MaterialTheme in your app:

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

To learn more about theming, check out the Theming in Compose guide.

Content slots

Material Components that support inner content (text labels, icons, etc.) tend to offer “slots” — generic lambdas that accept composable content — as well as public constants, like size and padding, to support laying out inner content to match Material specifications.

An example of this is Button:

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Figure 1. A Button using the content slot and default padding (left) and a Button using the content slot that provides a custom contentPadding (right).

Button has a generic content trailing lambda slot, which uses a RowScope to layout content composables in a row. It also has a contentPadding parameter to apply padding to the inner content. You can use constants provided through ButtonDefaults, or custom values.

Another example is ExtendedFloatingActionButton:

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

Figure 2. An ExtendedFloatingActionButton using the icon and text slots.

Instead of a generic content lambda, ExtendedFloatingActionButton has two slots for an icon and a text label. While each slot supports generic composable content, the component is opinionated about how these pieces of inner content are laid out. It handles padding, alignment, and size internally.

Scaffold

Compose provides convenient layouts for combining Material Components into common screen patterns. Composables such as Scaffold provide slots for various components and other screen elements.

Screen content

Scaffold has a generic content trailing lambda slot. The lambda receives an instance of PaddingValues that should be applied to the content root — for example, via Modifier.padding — to offset the top and bottom bars, if they exist.

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

App bars

Scaffold provides slots for a top app bar or a bottom app bar. The placement of the composables is handled internally.

You can use the topBar slot and a TopAppBar:

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

You can use the bottomBar slot and a BottomAppBar:

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

These slots can be used for other Material Components like BottomNavigation. You can also use custom composables — for an example, take a look at the onboarding screen from the Owl sample.

Floating action buttons

Scaffold provides a slot for a floating action button.

You can use the floatingActionButton slot and a FloatingActionButton:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

The bottom placement of the FAB composable is handled internally. You can use the floatingActionButtonPosition parameter to adjust the horizontal position:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

If you’re using the Scaffold composable’s bottomBar slot, you can use the isFloatingActionButtonDocked parameter to overlap the FAB with the bottom app bar:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

Figure 3. A Scaffold using the floatingActionButton slot and the bottomBar slot. The isFloatingActionButtonDocked parameter is set to false (top) and true (bottom).

BottomAppBar supports FAB cutouts with the cutoutShape parameter, which accepts any Shape. It’s a good idea to provide the same Shape used by the docked component. For example, FloatingActionButton uses MaterialTheme.shapes.small with a 50 percent corner size as the default value for its shape parameter:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

Figure 4. A Scaffold with a BottomAppBar and a docked FloatingActionButton. The BottomAppBar has a custom cutoutShape that matches the Shape used by the FloatingActionButton.

Snackbars

Scaffold provides a means to display snackbars.

This is provided via ScaffoldState, which includes a SnackbarHostState property. You can use rememberScaffoldState to create an instance of ScaffoldState that should be passed to Scaffold with the scaffoldState parameter. SnackbarHostState provides access to the showSnackbar function. This suspending function requires a CoroutineScope — for example, using rememberCoroutineScope — and can be called in response to UI events to show a Snackbar within Scaffold.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

You can provide an optional action and adjust the duration of the Snackbar. The snackbarHostState.showSnackbar function accepts additional actionLabel and duration parameters, and returns a SnackbarResult.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

You can provide a custom Snackbar with the snackbarHost parameter. See the SnackbarHost API reference docs for more information.

Drawers

Scaffold provides a slot for a modal navigation drawer. The draggable sheet and layout of the composable is handled internally.

You can use the drawerContent slot, which uses a ColumnScope to layout drawer content composables in a column:

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold accepts a number of additional drawer parameters. For example, you can toggle whether or not the drawer responds to drags with the drawerGesturesEnabled parameter:

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

Programmatically opening and closing the drawer is done via ScaffoldState, which includes a DrawerState property that should be passed to Scaffold with the scaffoldState parameter. DrawerState provides access to the open and close functions, as well as properties related to the current drawer state. These suspending functions require a CoroutineScope — for example, using rememberCoroutineScope — and can be called in response to UI events.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

If you want to implement a modal navigation drawer without a Scaffold, you can use the ModalDrawer composable. It accepts similar drawer parameters to Scaffold.

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

If you want to implement a bottom navigation drawer, you can use the BottomDrawer composable:

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

Bottom sheets

If you want to implement a standard bottom sheet, you can use the BottomSheetScaffold composable. It accepts similar parameters to Scaffold, such as topBar, floatingActionButton, and snackbarHost. It includes additional parameters that provide a means to display bottom sheets.

You can use the sheetContent slot, which uses a ColumnScope to layout sheet content composables in a column:

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold accepts a number of additional sheet parameters. For example, you can set the peek height of the sheet with the sheetPeekHeight parameter. You can also toggle whether or not the drawer responds to drags with the sheetGesturesEnabled parameter.

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

Programmatically expanding and collapsing the sheet is done via BottomSheetScaffoldState, which includes a BottomSheetState property. You can use rememberBottomSheetScaffoldState to create an instance of BottomSheetScaffoldState that should be passed to BottomSheetScaffold with the scaffoldState parameter. BottomSheetState provides access to the expand and collapse functions, as well as properties related to the current sheet state. These suspending functions require a CoroutineScope — for example, using rememberCoroutineScope — and can be called in response to UI events.

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

If you want to implement a modal bottom sheet, you can use the ModalBottomSheetLayout composable:

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

Backdrop

If you want to implement a backdrop, you can use the BackdropScaffold composable.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold accepts a number of additional backdrop parameters. For example, you can set the peek height of the back layer and the minimum inactive height of the front layer with the peekHeight and headerHeight parameters. You can also toggle whether or not the backdrop responds to drags with the gesturesEnabled parameter.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

Programmatically revealing and concealing the backdrop is done via BackdropScaffoldState. You can use rememberBackdropScaffoldState to create an instance of BackdropScaffoldState that should be passed to BackdropScaffold with the scaffoldState parameter. BackdropScaffoldState provides access to the reveal and conceal functions, as well as properties related to the current backdrop state. These suspending functions require a CoroutineScope — for example, using rememberCoroutineScope — and can be called in response to UI events.

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)