# FlexibleBottomSheet: a Compose bottom sheet you can size, drive and touch behind

> FlexibleBottomSheet is a Compose Multiplatform bottom sheet that adds what the standard ModalBottomSheet does not offer, per-state sizing, a non-modal mode where content behind the sheet stays interactive like Google Maps, and a state object you can drive programmatically. It is Apache-2.0 licensed, the current release is 0.4.0 from 2026-09-17, and it needs API 21 or newer.

**skydoves/FlexibleBottomSheet** — 🐬 Advanced Compose Multiplatform bottom sheet for segmented sizing, non-modal type, and allows interaction behind the bottom sheet similar to Google Maps.

- Repository: https://github.com/skydoves/FlexibleBottomSheet
- Website: https://skydoves.github.io/FlexibleBottomSheet/
- Stars: 1,134 · Forks: 54
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/skydoves-flexiblebottomsheet

## Three things the standard ModalBottomSheet does not do

FlexibleBottomSheet is described as an advanced Compose Multiplatform bottom sheet for segmented sizing, non-modal type, and allowing interaction behind the bottom sheet similar to Google Maps, with additional conveniences including specifying sheet sizes, monitoring sheet states and more customization.

The readme is explicit about the baseline. You can implement a flexible bottom sheet with `FlexibleBottomSheet`, similar to the `ModalBottomSheet` provided by Compose Material 3, and essentially you can achieve the same behavior as `ModalBottomSheet` by not altering any properties.

That sentence is the honest framing of the library, and it is worth taking seriously. If your sheet is a plain modal sheet that expands to the top and dismisses on a swipe, this library adds a dependency and nothing else. The value is in the three named capabilities.

Segmented sizing is the first, and it is the largest. A `FlexibleSheetSize` gives the sheet a height for each of its expanded states, so a sheet can be 90 percent of the screen when fully open, half when intermediate, and a small strip when slightly expanded. The Material 3 sheet has a single expanded height, so a design that needs a peek state has to fake it.

The non-modal type is the second. `isModal = false` lets content behind the sheet stay interactive, which is the Google Maps pattern where the map remains pannable while a card floats over it. In a modal sheet, the content behind is blocked, and the whole screen is treated as one focus scope.

The third is the state object, which is where most of the flexibility lives.

## The state object is what you actually hold on to

`FlexibleBottomSheetState` is described as a crucial concept that must be bound to `FlexibleBottomSheet` to manage its state changes, and it also enables you to customize UI/UX behaviours and take manual control over expanding and hiding the sheet. You remember it with `rememberFlexibleBottomSheetState`.

```kotlin
FlexibleBottomSheet(
  onDismissRequest = onDismissRequest,
  sheetState = rememberFlexibleBottomSheetState(
    skipSlightlyExpanded = false,
    skipIntermediatelyExpanded = false,
    isModal = true,
    allowNestedScroll = true,
    flexibleSheetSize = FlexibleSheetSize(
      fullyExpanded = 1.0f,
      intermediatelyExpanded = 0.5f,
      slightlyExpanded = 0.25f
    )
  ),
) {
  ..
}
```

The sizes are fractions, so the sheet's height is a proportion of the available space rather than a fixed dp value, which is what makes the same configuration work on a phone and a tablet.

The manual control is the part that changes how a sheet feels in an application. The example uses a coroutine scope and reads `sheetState.swipeableState.currentValue`, then dispatches on the current state:

```kotlin
when (sheetState.swipeableState.currentValue) {
  FlexibleSheetValue.SlightlyExpanded -> sheetState.intermediatelyExpand()
  FlexibleSheetValue.IntermediatelyExpanded -> sheetState.fullyExpand()
  else -> sheetState.hide()
}
```

So the sheet can be driven by a button, cycling through its states, and it can be read as well as written. That is the difference between a sheet the user controls and a sheet the application controls, and it is what you need for a button that opens the sheet to its full height or for a list row that expands inline.

The `allowNestedScroll` option in the example is the other flag worth knowing, since a sheet inside a scrolling parent needs to hand scrolling to the parent when it reaches its end.

## Four states, two of which you can skip

The sheet has four primary statuses, known as `FlexibleValues`, and the skip flags decide which of them exist at runtime.

Fully Expanded is the sheet at its fully-expanded height. The documentation states this one is mandatory and cannot be skipped.

Intermediately Expanded is the sheet at an intermediate height, and it can be skipped by setting `skipIntermediatelyExpanded` to `true`.

Slightly Expanded is the sheet at a slightly expanded height, and this one is skipped by default but can be enabled by setting `skipSlightlyExpanded` to `false`.

Hidden means the sheet is not visible on screen at all. If you never want to dismiss and want to keep the sheet displayed, you can give `skipHiddenState` the value `true`.

That last option is worth understanding on its own, because a sheet with no hidden state cannot be dismissed, which is the behaviour you want for a persistent panel and the behaviour that will surprise a user if you enable it by accident. The default set of states is therefore: fully, intermediate, hidden, with slightly expanded available if you ask for it.

The initial state is a separate parameter, `initialValue`, and it lets the sheet appear at a specific state without animating into it. The documentation is clear that the value has to be compatible with the skip settings, with the example being that an `initialValue` of SlightlyExpanded is wrong when `skipSlightlyExpanded` is `true`. That coupling is the one sharp edge in the API, and it is the kind of thing that should be a compile error and is not.

## Two artifacts, one for Material and one for Material 3

The distribution is split by which Compose Material version you use, and the split is visible in the module names.

```gradle
dependencies {
    // compose material
    implementation("com.github.skydoves:flexible-bottomsheet-material:0.4.0")

    // compose material3
    implementation("com.github.skydoves:flexible-bottomsheet-material3:0.4.0")
}
```

Both are published to Maven Central, and the search link in the readme points at the `com.github.skydoves` group.

For Kotlin Multiplatform, the same two artifacts go into the `commonMain` source set of your `build.gradle.kts`, with the version as a variable:

```gradle
sourceSets {
    val commonMain by getting {
        dependencies {
            // compose material
            implementation("com.github.skydoves:flexible-bottomsheet-material:$version")
            
            // compose material3
            implementation("com.github.skydoves:flexible-bottomsheet-material3:$version")
        }
    }
}
```

That the multiplatform instructions exist at all is the notable part. The library is described as Compose Multiplatform from the outset, and the repository layout backs that up with a `flexible-core/` module, the two themed modules `flexible-bottomsheet-material/` and `flexible-bottomsheet-material3/`, and a `kotlin-js-store/` directory. The `flexible-core` module is where the sheet and its state live, and the themed modules are the Material and Material 3 surfaces over it.

The API level is 21 or newer, and the repository is a full Gradle project with `buildSrc/`, `spotless/` for formatting, `renovate.json` for dependency updates, an `app/` sample, a `baselineprofile/` module with a matching `baselineprofile-app/`, `docs/` and an `mkdocs.yml`. The baseline profile modules are a detail that says something about the project's maturity: shipping a macrobenchmark baseline alongside a Compose library is the kind of thing a maintainer does when the library is on a hot path and the animation cost matters.

## Where FlexibleBottomSheet is the wrong choice

The first case is a sheet that does not need any of the three features. The readme says it plainly, that you achieve the same behaviour as Material 3's `ModalBottomSheet` by not altering any properties. If your sheet is a single-height modal that dismisses on swipe, the Material 3 component is already in your dependency tree, it is maintained by the Android team alongside the rest of Compose, and adding a third-party library for it is a cost with no return.

The second case is a project on Compose Multiplatform where the sheet needs to look native on iOS. The library is Compose Multiplatform, but the `isModal` and non-modal behaviours, and the segmented sizing, are described in Android terms, with the Google Maps comparison and an API level of 21 as the platform framing. A multiplatform team that needs a bottom sheet on both platforms has to check that the behaviour matches on both, and the documentation here does not describe the iOS behaviour in any detail.

The third is a team that wants a different interaction model entirely, such as a bottom bar that expands into a full panel with a shared element transition, or a sheet that is dragged rather than swiped between fixed stops. This library models the sheet as a set of discrete states with fixed sizes, which is the right model for the Google Maps pattern and the wrong one for a continuous expansion.

The fourth is a project that has already standardised on a sheet library. The ecosystem has several, and switching means rewriting every sheet's configuration, and the `initialValue` coupling to the skip flags means those settings are not as mechanical as they look.

One maintenance observation. The release line is 0.x, with 0.4.0 on 2026-09-17, 0.3.0 on 2026-07-02 and 0.2.0 on 2026-01-08, and the last push was on 2026-09-17. The library is moving, and the artifact names and the group id carry a version in the coordinate, so an upgrade is a coordinate change rather than a resolution, which is the safer pattern but also means you have to remember to do it.

## Conclusion

FlexibleBottomSheet fits an Android team that needs a sheet with several sizes, a sheet the user can interact with the content behind, or a sheet driven from a button rather than a swipe, and the non-modal mode in particular is the kind of thing you either need or you do not. It does not fit a project that just wants the Material 3 sheet, since the readme says you achieve the same behaviour as ModalBottomSheet by not altering any properties, which means the library is only worth its dependency when you change something. Before you adopt it, check the Compose Material 3 version you are on and pick the material or material3 artifact to match, and read the note about `initialValue` needing to be compatible with the skip settings, because an incompatible combination is a runtime surprise.

## FAQ

### How do I add FlexibleBottomSheet to a Gradle project?

Add the dependency for your Compose version to your module's build.gradle, either `com.github.skydoves:flexible-bottomsheet-material` or `com.github.skydoves:flexible-bottomsheet-material3`, both at version 0.4.0. For Kotlin Multiplatform, put the same artifact in the commonMain source set of your build.gradle.kts.

### What states can a FlexibleBottomSheet be in?

Four: Fully Expanded, which is mandatory and cannot be skipped, Intermediately Expanded, which can be skipped with `skipIntermediatelyExpanded`, Slightly Expanded, which is skipped by default and enabled with `skipSlightlyExpanded`, and Hidden, which can be disabled with `skipHiddenState` if you never want the sheet to dismiss.

### How do I control a FlexibleBottomSheet from code?

Hold a `FlexibleBottomSheetState` created with `rememberFlexibleBottomSheetState`, read its `swipeableState.currentValue` to see where it is, and call `intermediatelyExpand()`, `fullyExpand()` or `hide()` from a coroutine scope to move it.

### What does the non-modal mode of FlexibleBottomSheet do?

It lets the user interact with the content behind the sheet, in the way Google Maps does with a floating card. You enable it with `isModal = false` on the state, and the sheet stops blocking the rest of the screen.

## Sources

- [License: Apache-2.0](https://github.com/skydoves/FlexibleBottomSheet/blob/main/LICENSE)
- [Project website](https://skydoves.github.io/FlexibleBottomSheet/)
- [README](https://github.com/skydoves/FlexibleBottomSheet/blob/main/README.md)
- [Releases](https://github.com/skydoves/FlexibleBottomSheet/releases)
- [skydoves/FlexibleBottomSheet on GitHub](https://github.com/skydoves/FlexibleBottomSheet)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/skydoves-flexiblebottomsheet
