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
SheetDetentdefines a height where the sheet can rest.UnstyledBottomSheetrepresents the draggable container theSheetis dragged in.Sheetis the rendered bit of the sheet.DragIndicationadds 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? |