A custom implementation of Material 3's ModalBottomSheet that fixes critical issues with nested
scrolling and IME (keyboard) padding:
-
Issue #353304855: Fixed inconsistent nested scroll handling
- The official implementation incorrectly handles nested scroll events when the bottom sheet
contains scrollable components like
LazyColumn - The sheet attempts to dismiss prematurely, even when the inner content hasn't been fully scrolled to the top
- The official implementation incorrectly handles nested scroll events when the bottom sheet
contains scrollable components like
-
Issue #289824811: Customizable IME padding behavior
- The official implementation applies
imePadding()to the entire sheet, causing unwanted bottom padding when the keyboard appears
- The official implementation applies
Add the JitPack repository to your settings.gradle.kts (or root build.gradle.kts if using older Gradle):
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("com.github.lucf15:ModalBottomSheet:1.0.1")
}- Minimum SDK: 21
- Compile SDK: 35
@Composable
fun ModalBottomSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
sheetGesturesEnabled: Boolean = true,
nestedScrollableState: ScrollableState? = null,
shape: Shape = androidx.compose.material3.BottomSheetDefaults.ExpandedShape,
containerColor: Color = androidx.compose.material3.BottomSheetDefaults.ContainerColor,
contentColor: Color = contentColorFor(containerColor),
tonalElevation: Dp = 0.dp,
scrimColor: Color = androidx.compose.material3.BottomSheetDefaults.ScrimColor,
dragHandle: @Composable (() -> Unit)? = { androidx.compose.material3.BottomSheetDefaults.DragHandle() },
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties,
showMotion: FiniteAnimationSpec<Float> = BottomSheetAnimationSpec,
hideMotion: FiniteAnimationSpec<Float> = BottomSheetAnimationSpec,
anchoredDraggableMotionSpec: FiniteAnimationSpec<Float> = BottomSheetAnimationSpec,
content: @Composable ColumnScope.() -> Unit,
)Migrating from the official Material 3 ModalBottomSheet is straightforward:
- Update your import:
// Before
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
// After
import it.lucf15.compose.bottomsheet.ModalBottomSheet
import it.lucf15.compose.bottomsheet.rememberModalBottomSheetState- If you have scrollable content, pass the scroll state:
val scrollState = rememberLazyListState()
ModalBottomSheet(
// ... other parameters
nestedScrollableState = scrollState // Add this
) {
LazyColumn(state = scrollState) {
// Your content
}
}- If you want to control IME padding:
ModalBottomSheet(
// ... other parameters
) {
Box(Modifier.imePadding()) { // Add imePadding when you need it
// Your content
}
}The library implements a custom ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection that
properly coordinates scroll events between the bottom sheet and nested scrollable content. It
ensures:
- The inner scrollable component receives scroll events first
- The bottom sheet only responds to dismiss gestures when the inner content is at the top
- Smooth fling gestures are preserved across both components
Instead of applying Modifier.imePadding() unconditionally on the root ModalBottomSheet, the
library lets users place it where needed.
Copyright 2025 lucf15
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.