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 Design systems in Compose guides.

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(title = {
            Text("My App")
        })
    }
) { contentPadding ->
    // Screen content
}

You can use the bottomBar slot and a BottomAppBar:

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) { contentPadding ->
    // 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 */
        }
    }
) { contentPadding ->
    // 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 */
        }
    },
    floatingActionButtonPosition = FabPosition.Center
) { contentPadding ->
    // Screen content
}

Snackbars

Scaffold provides a means to display snackbars.

This is provided via SnackbarHost, which includes a SnackbarHostState property. 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 scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
    snackbarHost = {
        SnackbarHost(hostState = snackbarHostState)
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            icon = { Icon(Icons.Filled.Image, contentDescription = "") },
            onClick = {
                scope.launch {
                    snackbarHostState.showSnackbar("Snackbar")
                }
            }
        )
    }
) { contentPadding ->
    // 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 scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
    snackbarHost = {
        SnackbarHost(hostState = snackbarHostState)
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            icon = { Icon(Icons.Filled.Image, contentDescription = "") },
            onClick = {
                scope.launch {
                    val result = 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 */
                        }
                    }
                }
            }
        )
    }
) { contentPadding ->
    // Screen content
}

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

Drawers

ModalNavigationDrawer is the Material Design navigation drawer.

You can use the drawerContent slot to provide a ModalDrawerSheet and provide the drawer's contents:

ModalNavigationDrawer(
    drawerContent = {
        ModalDrawerSheet {
            Text("Drawer title", modifier = Modifier.padding(16.dp))
            Divider()
            NavigationDrawerItem(
                label = { Text(text = "Drawer Item") },
                selected = false,
                onClick = { /*TODO*/ }
            )
            // ...other drawer items
        }
    }
) {
    // Screen content
}

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

ModalNavigationDrawer(
    drawerContent = {
        ModalDrawerSheet {
            // Drawer contents
        }
    },
    gesturesEnabled = false
) {
    // Screen content
}

Programmatically opening and closing the drawer is done via DrawerState. A DrawerState should be passed to ModalNavigationDrawer with the drawerState 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 drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
    drawerState = drawerState,
    drawerContent = {
        ModalDrawerSheet { /* Drawer content */ }
    },
) {
    Scaffold(
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Show drawer") },
                icon = { Icon(Icons.Filled.Add, contentDescription = "") },
                onClick = {
                    scope.launch {
                        drawerState.apply {
                            if (isClosed) open() else close()
                        }
                    }
                }
            )
        }
    ) { contentPadding ->
        // Screen content
    }
}

Bottom sheets

If you want to implement a bottom sheet, you can use the ModalBottomSheet composable.

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

ModalBottomSheet(onDismissRequest = { /* Executed when the sheet is dismissed */ }) {
    // Sheet content
}

Programmatically expanding and collapsing the sheet is done via SheetState. You can use rememberSheetState to create an instance of SheetState that should be passed to ModalBottomSheet with the sheetState parameter. SheetState provides access to the show 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. Make sure to remove the ModalBottomSheet from composition upon hiding the bottom sheet.

val sheetState = rememberModalBottomSheetState()
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
Scaffold(
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show bottom sheet") },
            icon = { Icon(Icons.Filled.Add, contentDescription = "") },
            onClick = {
                showBottomSheet = true
            }
        )
    }
) { contentPadding ->
    // Screen content

    if (showBottomSheet) {
        ModalBottomSheet(
            onDismissRequest = {
                showBottomSheet = false
            },
            sheetState = sheetState
        ) {
            // Sheet content
            Button(onClick = {
                scope.launch { sheetState.hide() }.invokeOnCompletion {
                    if (!sheetState.isVisible) {
                        showBottomSheet = false
                    }
                }
            }) {
                Text("Hide bottom sheet")
            }
        }
    }
}