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

Bottom Sheet

A draggable bottom sheet with custom detents.

import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragIndication
import com.composeunstyled.Sheet
import com.composeunstyled.SheetDetent
import com.composeunstyled.SheetDetent.Companion.FullyExpanded
import com.composeunstyled.UnstyledBottomSheet
import com.composeunstyled.rememberBottomSheetState

@Composable
fun BottomSheetDemo() {
  val Peek = SheetDetent("peek") { containerHeight, _ ->
    containerHeight * 0.6f
  }
  val sheetState = rememberBottomSheetState(
    initialDetent = Peek,
    detents = listOf(Peek, FullyExpanded),
  )
  UnstyledBottomSheet(
    state = sheetState,
    modifier = Modifier.fillMaxSize().padding(top = 12.dp),
  ) {
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
      Sheet(
        modifier = Modifier
          .clip(RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp))
          .background(Color(0xFFF8FAFC))
          .border(1.dp, Color(0xFFCACACA), RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp))
          .widthIn(max = 640.dp)
          .fillMaxWidth(),
      ) {
        Box(Modifier.fillMaxWidth().height(600.dp)) {
          DragIndication(
            modifier = Modifier
              .align(Alignment.TopCenter)
              .padding(top = 22.dp)
              .background(Color(0xFFCACACA), RoundedCornerShape(100))
              .size(32.dp, 4.dp),
            indication = LocalIndication.current,
          )
        }
      }
    }
  }
}

Features

  • Custom detents
  • Soft-keyboard support
  • Dynamic content sizing
  • Scrollable content without fixed height

Installation

implementation("com.composables:composeunstyled-bottom-sheet")

Anatomy

val sheetState = rememberBottomSheetState(
  initialDetent = SheetDetent.Hidden,
)

UnstyledBottomSheet(state = sheetState) {
  Sheet {
    DragIndication()
  }
}

Concepts

  • SheetDetent defines a height where the sheet can rest.
  • UnstyledBottomSheet represents the draggable container the Sheet is dragged in.
  • Sheet is the rendered bit of the sheet.
  • DragIndication adds an interactive handle for expand, collapse, and dismiss actions.

Accessibility

Use the DragIndication component when your sheet can move between multiple detents. It provides semantic expand, collapse, and dismiss actions so users can control the sheet without dragging.

Code Examples

Showing and hiding the sheet

Use the targetDetent property to animate the sheet to a new detent:

val sheetState = rememberBottomSheetState(
  initialDetent = SheetDetent.Hidden,
)

BasicText(
  text = "Show sheet",
  modifier = Modifier.clickable {
    sheetState.targetDetent = SheetDetent.FullyExpanded
  }
)

UnstyledBottomSheet(state = sheetState) {
  Sheet {
    BasicText(
      text = "Hide sheet",
      modifier = Modifier.clickable {
        sheetState.targetDetent = SheetDetent.Hidden
      }
    )
  }
}

Waiting for the sheet animation

Use the suspend animateTo() function to wait until the sheet animation is done:

val scope = rememberCoroutineScope()

BasicText(
  text = "Show sheet",
  modifier = Modifier.clickable {
    scope.launch {
      sheetState.animateTo(SheetDetent.FullyExpanded)
    }
  }
)

Moving the sheet instantly

Use the jumpTo() function to move to a detent without animation:

BasicText(
  text = "Open immediately",
  modifier = Modifier.clickable {
    sheetState.jumpTo(SheetDetent.FullyExpanded)
  }
)

Creating sheets with custom detents

Use the SheetDetent constructor to create a new detent. Pass a unique identifier and a function that calculates the detent height. The calculated height cannot be smaller than 0.dp, taller than the container, or taller than the sheet content.

Keep this calculation fast. It runs during sheet measurement.

Make sure to pass your new detent when creating your bottom sheet state:

val Peek = SheetDetent("peek") { containerHeight, sheetHeight ->
  containerHeight * 0.6f
}

val sheetState = rememberBottomSheetState(
  initialDetent = Peek,
  detents = listOf(SheetDetent.Hidden, Peek, SheetDetent.FullyExpanded),
)

UnstyledBottomSheet(state = sheetState) {
  Sheet {
    DragIndication()
  }
}

Updating detents after the state is created

Use the invalidateDetents() function to recalculate sheet detents. This is useful when a custom detent reads a measured value that can change, such as a header height:

val peekHeight = remember { mutableStateOf(96.dp) }
val Peek = remember {
  SheetDetent("peek") { _, _ -> peekHeight.value }
}

val sheetState = rememberBottomSheetState(
  initialDetent = Peek,
  detents = listOf(Peek, SheetDetent.FullyExpanded),
)

LaunchedEffect(peekHeight.value) {
  sheetState.invalidateDetents()
}

Using scrollable sheet content

Use a scrollable layout inside the Sheet component to make content scroll within the current detent height:

Working with the soft keyboard

Use the offsetForIme parameter to automatically move the sheet above the soft keyboard:

var value by remember { mutableStateOf("") }

UnstyledBottomSheet(
  state = sheetState,
  offsetForIme = true,
) {
  Sheet {
    BasicTextField(
      value = value,
      onValueChange = { value = it },
    )
  }
}

Customizing sheet animation between detents

Use the animationSpec parameter to customize the default animation between detents:

val sheetState = rememberBottomSheetState(
  initialDetent = SheetDetent.Hidden,
  animationSpec = spring(
    dampingRatio = Spring.DampingRatioMediumBouncy,
    stiffness = Spring.StiffnessLow,
  ),
)

API Reference

rememberBottomSheetState

Parameter Type Description
initialDetent SheetDetent A SheetDetent which controls the height in which the sheet will be introduced within its container.
detents List<SheetDetent>
animationSpec AnimationSpec<Float> An AnimationSpec used when animating the sheet across the different sheetDetents.
confirmDetentChange (SheetDetent) -> Boolean
decayAnimationSpec DecayAnimationSpec<Float>
velocityThreshold () -> Dp
positionalThreshold (totalDistance: Dp) -> Dp

BottomSheetState

Parameter Type Description
confirmDetentChange (SheetDetent) -> Boolean
detents List<SheetDetent>
currentDetent SheetDetent The SheetDetent in which the sheet is currently rested on. Setting a new detent will cause the sheet to animate to that detent.
targetDetent SheetDetent The SheetDetent in which the sheet is about to rest on, if it is being dragged or animated.
isIdle Boolean Whether the sheet is currently resting at a specific detent.
offset Float The current offset of the sheet.
fun progress() (SheetDetent, SheetDetent) -> Float
suspend fun animateTo() suspend (SheetDetent, AnimationSpec<Float>?) -> Unit Animates the sheet to the given detent. This is a suspend function, which you can use to wait until the animation is complete.
fun jumpTo() (SheetDetent) -> Unit Makes the sheet to immediately appear to the given detent without any animation.
fun invalidateDetents() () -> Unit

BottomSheet

Parameter Type Description
state BottomSheetState The BottomSheetState for the component
modifier Modifier The Modifier for the component
enabled Boolean Enables or disables dragging.
offsetForIme Boolean
content BottomSheetScope.() -> Unit The contents of the sheet.

BottomSheetScope.Sheet

Parameter Type Description
modifier Modifier The Modifier for the component
content () -> Unit The contents of the sheet.

BottomSheetScope.DragIndication

Parameter Type Description
modifier Modifier The Modifier for the component
indication Indication?
interactionSource MutableInteractionSource?