---
title: Modal Bottom Sheet
description: A dismissible modal bottom sheet with custom detents.
---

> **Recommendation:** For new interfaces, use [Drawer](https://composeunstyled.com/docs/components.mddrawer/) instead. It is a more flexible abstraction and the recommended API moving forward.



```kotlin expandable title="ModalBottomSheetDemo.kt" githubUrl="https://github.com/composablehorizons/compose-unstyled/blob/main/demo/src/commonMain/kotlin/com/composeunstyled/demo/modalbottomsheet/ModalBottomSheetDemo.kt"
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.composeunstyled.DragIndication
import com.composeunstyled.Scrim
import com.composeunstyled.Sheet
import com.composeunstyled.SheetDetent
import com.composeunstyled.SheetDetent.Companion.FullyExpanded
import com.composeunstyled.SheetDetent.Companion.Hidden
import com.composeunstyled.Text
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledModalBottomSheet
import com.composeunstyled.demo.borderToken
import com.composeunstyled.demo.colors
import com.composeunstyled.demo.scrimToken
import com.composeunstyled.demo.surfaceToken
import com.composeunstyled.rememberModalBottomSheetState
import com.composeunstyled.theme.Theme

@Preview
@Composable
fun ModalBottomSheetDemo() {
  val Peek = SheetDetent("peek") { containerHeight, _ ->
    containerHeight * 0.6f
  }
  val modalSheetState = rememberModalBottomSheetState(
    initialDetent = Peek,
    detents = listOf(Hidden, Peek, FullyExpanded),
  )

  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center,
  ) {
    UnstyledButton(
      onClick = { modalSheetState.targetDetent = Peek },
      modifier = Modifier
        .heightIn(32.dp)
        .background(Theme[colors][surfaceToken])
        .border(1.dp, Theme[colors][borderToken]),
      contentPadding = PaddingValues(horizontal = 10.dp),
      indication = LocalIndication.current,
    ) {
      Text("Show bottom sheet")
    }

    UnstyledModalBottomSheet(
      state = modalSheetState,
      overlay = {
        Scrim(
          scrimColor = Theme[colors][scrimToken],
          enter = fadeIn(),
          exit = fadeOut(),
        )
      },
    ) {
      Box(
        modifier = Modifier
          .fillMaxWidth(),
        contentAlignment = Alignment.TopCenter,
      ) {
        Sheet(
          modifier = Modifier
            .widthIn(max = 640.dp)
            .fillMaxWidth()
            .background(Theme[colors][surfaceToken])
            .border(1.dp, Theme[colors][borderToken]),
        ) {
          Box(
            modifier = Modifier.fillMaxWidth().height(1000.dp),
            contentAlignment = Alignment.TopCenter,
          ) {
            DragIndication(
              modifier = Modifier
                .padding(top = 22.dp)
                .background(Theme[colors][borderToken])
                .size(32.dp, 4.dp),
              indication = LocalIndication.current,
            )
          }
        }
      }
    }
  }
}
```



## Features

- Custom detents
- Custom overlay content
- Back press and outside click dismissal
- Dynamic content sizing
- Soft-keyboard support
- Scrollable content without fixed height

## Installation

```kotlin
implementation("com.composables:composeunstyled-modal-bottom-sheet:2.10.0")
```

## Anatomy

```kotlin
val sheetState = rememberModalBottomSheetState(
  initialDetent = SheetDetent.Hidden,
)

UnstyledModalBottomSheet(state = sheetState) {
  Sheet {
    DragIndication()
  }
}
```

## Concepts

- `SheetDetent` defines a height where the sheet can rest.
- `UnstyledModalBottomSheet` represents the modal layer the sheet is rendered in.
- `Sheet` is the rendered bit of the sheet.
- `DragIndication` adds an interactive handle for expand, collapse, and dismiss actions.

## Accessibility

Use `DragIndication` 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 a modal bottom sheet

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

```kotlin expandable
val sheetState = rememberModalBottomSheetState(
  initialDetent = SheetDetent.Hidden,
)

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

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

### Waiting for modal bottom sheet animations

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

```kotlin expandable
val scope = rememberCoroutineScope()

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

### Opening a modal bottom sheet instantly

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

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

### Adding an overlay behind a modal bottom sheet

Use the `overlay` parameter to render content behind the modal sheet. `Scrim` provides a
ready-made overlay for modal bottom sheets.

```kotlin expandable
UnstyledModalBottomSheet(
  state = sheetState,
  overlay = { Scrim() },
) {
  Sheet {
    BasicText("Sheet content")
  }
}
```

### Creating modal bottom 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.

```kotlin expandable
val Peek = SheetDetent("peek") { containerHeight, sheetHeight ->
  minOf(containerHeight * 0.4f, sheetHeight)
}

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

### Updating modal bottom sheet detents after layout changes

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:

```kotlin expandable
var peekHeight by remember { mutableStateOf(120.dp) }

val Peek = remember {
  SheetDetent("peek") { _, _ -> peekHeight }
}

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

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

### Building modal bottom sheets with scrollable content

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

### Moving a modal bottom sheet above the soft keyboard

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

```kotlin expandable
val textState = rememberTextFieldState()

UnstyledModalBottomSheet(
  state = sheetState,
  properties = ModalBottomSheetProperties(offsetForIme = true),
) {
  Sheet {
    BasicTextField(state = textState)
  }
}
```

### Disabling back press and outside click dismissal

Use the `properties` parameter to control how the modal sheet can be dismissed:

```kotlin expandable
UnstyledModalBottomSheet(
  state = sheetState,
  properties = ModalBottomSheetProperties(
    dismissOnBackPress = false,
    dismissOnClickOutside = false,
  ),
) {
  Sheet {
    BasicText("Sheet content")
  }
}
```

### Reacting to modal bottom sheet dismissal

Use the `onDismiss` parameter to run code when the sheet is dismissed:

```kotlin expandable
UnstyledModalBottomSheet(
  state = sheetState,
  onDismiss = { selectedItem = null },
) {
  Sheet {
    BasicText("Sheet content")
  }
}
```

### Customizing modal bottom sheet animation between detents

Use the `animationSpec` parameter to customize the default animation between detents. Use the
`dismissAnimationSpec` parameter to customize the animation used when the modal sheet is dismissed:

```kotlin expandable
val sheetState = rememberModalBottomSheetState(
  initialDetent = SheetDetent.Hidden,
  animationSpec = tween(durationMillis = 300),
  dismissAnimationSpec = tween(durationMillis = 180),
)
```

## API Reference

### rememberModalBottomSheetState

| Parameter | Type | Description |
|-----------|------|-------------|
| `initialDetent` | `SheetDetent` | A `SheetDetent` which controls the height in which the sheet will be introduced within its container. |
| `detents` | `List<SheetDetent>` | A list of `SheetDetent` which the sheet can be rested for dragging purposes. |
| `animationSpec` | `AnimationSpec<Float>` | An `AnimationSpec` used when animating the sheet across the different *sheetDetents*. |
| `dismissAnimationSpec` | `AnimationSpec<Float>?` |  |
| `velocityThreshold` | `() -> Dp` | The velocity threshold (in px per second) that the end velocity has to exceed in order to animate to the next state, even if the `positionalThreshold` has not been reached. |
| `positionalThreshold` | `(totalDistance: Dp) -> Dp` | The positional threshold, in px, to be used when calculating the target state while a drag is in progress and when settling after the drag ends. This is the distance from the start of a transition. It will be, depending on the direction of the interaction, added or subtracted from/to the origin offset. It should always be a positive value. |
| `confirmDetentChange` | `(SheetDetent) -> Boolean` |  |
| `decayAnimationSpec` | `DecayAnimationSpec<Float>` |  |
### ModalBottomSheetState

| Parameter | Type | Description |
|-----------|------|-------------|
| `bottomSheetState` | `BottomSheetState` |  |
| `modalState` | `ModalState` |  |
| `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) -> 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` |  |
### UnstyledModalBottomSheet

| Parameter | Type | Description |
|-----------|------|-------------|
| `state` | `ModalBottomSheetState` | The `ModalBottomSheetState` for the component |
| `enabled` | `Boolean` | Enables or disables dragging. |
| `properties` | `ModalBottomSheetProperties` | `ModalSheetProperties` that control whether the sheet needs to be dismissed on clicked outside, etc. |
| `onDismiss` | `() -> Unit` | Called when the sheet is being dismissed either by tapping outside or by pressing `Esc` or `Back`. |
| `overlay` | `(ModalBottomSheetOverlayScope.() -> Unit)?` |  |
| `content` | `ModalBottomSheetScope.() -> Unit` | The contents of the Modal Bottom Sheet. |
### ModalBottomSheetOverlayScope.Scrim

| Parameter | Type | Description |
|-----------|------|-------------|
| `modifier` | `Modifier` | The `Modifier` for the component |
| `scrimColor` | `Color` |  |
| `enter` | `EnterTransition` |  |
| `exit` | `ExitTransition` |  |
### ModalBottomSheetScope.Sheet

| Parameter | Type | Description |
|-----------|------|-------------|
| `modifier` | `Modifier` | The `Modifier` for the component |
| `content` | `() -> Unit` | The contents of the sheet. |
### ModalBottomSheetScope.DragIndication

| Parameter | Type | Description |
|-----------|------|-------------|
| `modifier` | `Modifier` |  |
| `indication` | `Indication?` |  |
| `interactionSource` | `MutableInteractionSource?` |  |
