Compose Unstyled 2.0 is out! Check the official announcement blog ->

Bottom Sheet

Modal bottom sheets for action menus, confirmations, and lightweight forms.

Use bottom sheets for contextual actions and short tasks that should stay close to the current screen.

View on GitHub
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.Copy
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Pencil
import com.composables.icons.lucide.Share
import com.composables.icons.lucide.Trash2
import com.composables.ui.components.BottomSheet
import com.composables.ui.components.BottomSheetDetent
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Icon
import com.composables.ui.components.Text
import com.composables.ui.components.rememberBottomSheetState
import com.composables.ui.theme.colors
import com.composables.ui.theme.destructiveColor
import com.composeunstyled.ProvideContentColor
import com.composeunstyled.theme.Theme

@Composable
fun BottomSheetActionMenuExample() {
    val sheetState = rememberBottomSheetState(
        detents = listOf(BottomSheetDetent.Hidden, BottomSheetDetent.FullyExpanded),
    )

    Button(onClick = { sheetState.targetDetent = BottomSheetDetent.FullyExpanded }) {
        Text("Open actions")
    }

    BottomSheet(
        state = sheetState,
        onDismissRequest = { sheetState.targetDetent = BottomSheetDetent.Hidden },
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 4.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Secondary,
                contentPadding = PaddingValues(horizontal = 16.dp),
            ) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    Text("Rename")
                    Spacer(Modifier.weight(1f))
                    Icon(
                        imageVector = Lucide.Pencil,
                        contentDescription = null,
                        modifier = Modifier.size(16.dp),
                    )
                }
            }
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Secondary,
                contentPadding = PaddingValues(horizontal = 16.dp),
            ) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    Text("Duplicate")
                    Spacer(Modifier.weight(1f))
                    Icon(
                        imageVector = Lucide.Copy,
                        contentDescription = null,
                        modifier = Modifier.size(16.dp),
                    )
                }
            }
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Secondary,
                contentPadding = PaddingValues(horizontal = 16.dp),
            ) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    Text("Share")
                    Spacer(Modifier.weight(1f))
                    Icon(
                        imageVector = Lucide.Share,
                        contentDescription = null,
                        modifier = Modifier.size(16.dp),
                    )
                }
            }
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Secondary,
                contentPadding = PaddingValues(horizontal = 16.dp),
            ) {
                ProvideContentColor(Theme[colors][destructiveColor]) {
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.spacedBy(12.dp),
                        verticalAlignment = Alignment.CenterVertically,
                    ) {
                        Text("Delete")
                        Spacer(Modifier.weight(1f))
                        Icon(
                            imageVector = Lucide.Trash2,
                            contentDescription = null,
                            modifier = Modifier.size(16.dp),
                        )
                    }
                }
            }
        }
    }
}

Installation

implementation("com.composables:ui:0.1.0")

Examples

Confirmation

View on GitHub
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composables.icons.lucide.Bell
import com.composables.icons.lucide.Lucide
import com.composables.ui.components.BottomSheet
import com.composables.ui.components.BottomSheetDetent
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Icon
import com.composables.ui.components.Text
import com.composables.ui.components.rememberBottomSheetState
import com.composables.ui.theme.colors
import com.composables.ui.theme.mutedColor
import com.composeunstyled.LocalTextStyle
import com.composeunstyled.ProvideContentColor
import com.composeunstyled.ProvideTextStyle
import com.composeunstyled.theme.Theme

@Composable
fun BottomSheetConfirmationExample() {
    val sheetState = rememberBottomSheetState(
        detents = listOf(BottomSheetDetent.Hidden, BottomSheetDetent.FullyExpanded),
    )

    Button(onClick = { sheetState.targetDetent = BottomSheetDetent.FullyExpanded }) {
        Text("Show confirmation")
    }

    BottomSheet(
        state = sheetState,
        onDismissRequest = { sheetState.targetDetent = BottomSheetDetent.Hidden },
        toolbar = {
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(16.dp),
            ) {
                Icon(
                    imageVector = Lucide.Bell,
                    contentDescription = null,
                    modifier = Modifier.size(28.dp),
                )
                Text("Allow notifications?")
            }
        },
        footer = {
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Primary,
            ) {
                Text("Allow")
            }
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Secondary,
            ) {
                Text("Maybe later")
            }
        },
    ) {
        ProvideContentColor(Theme[colors][mutedColor]) {
            ProvideTextStyle(LocalTextStyle.current.merge(TextStyle(fontSize = 16.sp, lineHeight = 24.sp))) {
                Text(
                    text = "Get notified when projects finish syncing, comments mention you, or billing needs attention.",
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.Center,
                )
            }
        }
    }
}

Form

View on GitHub
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import com.composables.ui.components.BottomSheet
import com.composables.ui.components.BottomSheetDetent
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Icon
import com.composables.ui.components.Text
import com.composables.ui.components.TextField
import com.composables.ui.components.rememberBottomSheetState
import com.composables.ui.theme.colors
import com.composables.ui.theme.mutedColor
import com.composeunstyled.theme.Theme

@Composable
fun BottomSheetFormExample() {
    val nameState = rememberTextFieldState("Alex Styl")
    val usernameState = rememberTextFieldState("alexstyl")
    val sheetState = rememberBottomSheetState(
        detents = listOf(BottomSheetDetent.Hidden, BottomSheetDetent.FullyExpanded),
    )

    Button(onClick = { sheetState.targetDetent = BottomSheetDetent.FullyExpanded }) {
        Text("Edit name")
    }

    BottomSheet(
        state = sheetState,
        onDismissRequest = { sheetState.targetDetent = BottomSheetDetent.Hidden },
        toolbar = { Text("Edit name") },
        footer = {
            Button(
                onClick = { sheetState.targetDetent = BottomSheetDetent.Hidden },
                modifier = Modifier.fillMaxWidth(),
                style = ButtonStyle.Primary,
            ) {
                Text("Save")
            }
        },
    ) {
        Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Text("Name")
                TextField(
                    state = nameState,
                    modifier = Modifier.fillMaxWidth(),
                    accessibilityLabel = "Name",
                    leading = {
                        Icon(
                            imageVector = User,
                            contentDescription = null,
                            modifier = Modifier.size(16.dp),
                            tint = Theme[colors][mutedColor],
                        )
                    },
                )
            }
            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Text("Username")
                TextField(
                    state = usernameState,
                    modifier = Modifier.fillMaxWidth(),
                    accessibilityLabel = "Username",
                    leading = {
                        Icon(
                            imageVector = AtSign,
                            contentDescription = null,
                            modifier = Modifier.size(16.dp),
                            tint = Theme[colors][mutedColor],
                        )
                    },
                )
            }
        }
    }
}

val AtSign: ImageVector
    get() {
        if (_AtSign != null) return _AtSign!!

        _AtSign = ImageVector.Builder(
            name = "at-sign",
            defaultWidth = 24.dp,
            defaultHeight = 24.dp,
            viewportWidth = 24f,
            viewportHeight = 24f
        ).apply {
            path(
                fill = SolidColor(Color.Transparent),
                stroke = SolidColor(Color.Black),
                strokeLineWidth = 2f,
                strokeLineCap = StrokeCap.Round,
                strokeLineJoin = StrokeJoin.Round
            ) {
                moveTo(16f, 12f)
                arcTo(4f, 4f, 0f, false, true, 12f, 16f)
                arcTo(4f, 4f, 0f, false, true, 8f, 12f)
                arcTo(4f, 4f, 0f, false, true, 16f, 12f)
                close()
            }
            path(
                fill = SolidColor(Color.Transparent),
                stroke = SolidColor(Color.Black),
                strokeLineWidth = 2f,
                strokeLineCap = StrokeCap.Round,
                strokeLineJoin = StrokeJoin.Round
            ) {
                moveTo(16f, 8f)
                verticalLineToRelative(5f)
                arcToRelative(3f, 3f, 0f, false, false, 6f, 0f)
                verticalLineToRelative(-1f)
                arcToRelative(10f, 10f, 0f, true, false, -4f, 8f)
            }
        }.build()

        return _AtSign!!
    }

private var _AtSign: ImageVector? = null


val User: ImageVector
    get() {
        if (_User != null) return _User!!

        _User = ImageVector.Builder(
            name = "user",
            defaultWidth = 24.dp,
            defaultHeight = 24.dp,
            viewportWidth = 24f,
            viewportHeight = 24f
        ).apply {
            path(
                fill = SolidColor(Color.Transparent),
                stroke = SolidColor(Color.Black),
                strokeLineWidth = 2f,
                strokeLineCap = StrokeCap.Round,
                strokeLineJoin = StrokeJoin.Round
            ) {
                moveTo(19f, 21f)
                verticalLineToRelative(-2f)
                arcToRelative(4f, 4f, 0f, false, false, -4f, -4f)
                horizontalLineTo(9f)
                arcToRelative(4f, 4f, 0f, false, false, -4f, 4f)
                verticalLineToRelative(2f)
            }
            path(
                fill = SolidColor(Color.Transparent),
                stroke = SolidColor(Color.Black),
                strokeLineWidth = 2f,
                strokeLineCap = StrokeCap.Round,
                strokeLineJoin = StrokeJoin.Round
            ) {
                moveTo(16f, 7f)
                arcTo(4f, 4f, 0f, false, true, 12f, 11f)
                arcTo(4f, 4f, 0f, false, true, 8f, 7f)
                arcTo(4f, 4f, 0f, false, true, 16f, 7f)
                close()
            }
        }.build()

        return _User!!
    }

private var _User: ImageVector? = null

API Reference

rememberBottomSheetState

Creates and remembers a [BottomSheetState] to be used in a BottomSheet.

@Composable
fun rememberBottomSheetState(
    initialDetent: BottomSheetDetent = BottomSheetDetent.Hidden,
    detents: List<BottomSheetDetent> = listOf(BottomSheetDetent.Hidden, BottomSheetDetent.FullyExpanded),
): BottomSheetState
Parameter Type Description
initialDetent BottomSheetDetent Detent to be used when the bottom sheet state is first created.
detents List<BottomSheetDetent> Available detents that the bottom sheet can move between.

A modal bottom sheet with optional header and footer content.

@Composable
fun BottomSheet(
    state: BottomSheetState,
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    toolbar: (@Composable () -> Unit)? = null,
    footer: (@Composable ColumnScope.() -> Unit)? = null,
    shape: Shape = Theme[shapes][sheetShape],
    backgroundColor: Color = Theme[colors][panelColor],
    contentColor: Color = Theme[colors][onPanelColor],
    contentPadding: PaddingValues = PaddingValues(20.dp),
    shadow: Shadow = Theme[shadows][overlayShadow],
    body: @Composable ColumnScope.() -> Unit,
)
Parameter Type Description
state BottomSheetState State object that controls the bottom sheet.
onDismissRequest () -> Unit Called when the sheet should dismiss.
modifier Modifier Modifier applied to the bottom sheet container.
toolbar (@Composable () -> Unit)? Optional header content shown at the top of the sheet.
footer (@Composable ColumnScope.() -> Unit)? Optional footer content shown below the sheet body.
shape Shape Shape used for the sheet container.
backgroundColor Color Background color used for the sheet surface.
contentColor Color Color used for sheet content.
contentPadding PaddingValues Padding applied around the sheet body content.
shadow Shadow Shadow applied to the sheet container.
body @Composable ColumnScope.() -> Unit Main content displayed inside the sheet.

BottomSheetDetent

Supported resting positions for the bottom sheet.

@JvmInline
value class BottomSheetDetent internal constructor(
    @Suppress("unused") private val value: Int,
)
Value Description
Hidden Keeps the bottom sheet off screen.
FullyExpanded Shows the bottom sheet at its expanded position.

animateTo

Animates a BottomSheetState to the requested detent.

suspend fun animateTo(detent: BottomSheetDetent)
Parameter Type Description
detent BottomSheetDetent Target detent to animate the sheet toward.

show

Animates the bottom sheet to its fully expanded state.

suspend fun show()

hide

Animates the bottom sheet to its hidden state.

suspend fun hide()