ModalBottomSheet displays a modal bottom sheet that slides up from the bottom of the screen. It includes a drag handle and supports gestures for dismissal.
Parameters
modifier
Modifier
default:"Modifier"
Modifier to be applied to the bottom sheet
sheetState
SheetState
default:"rememberModalBottomSheetState()"
State of the bottom sheet for controlling expansion
Whether the bottom sheet is currently visible
Callback invoked when the user dismisses the sheet
Whether gestures for dragging and dismissing are enabled
dragHandle
@Composable (() -> Unit)?
default:"BottomSheetDefaults.DragHandle()"
Optional drag handle composable. Pass null to hide the drag handle
content
@Composable ColumnScope.() -> Unit
required
Content to display inside the bottom sheet
Usage Examples
Basic Modal Bottom Sheet
var showBottomSheet by remember { mutableStateOf(false) }
Button(onClick = { showBottomSheet = true }) {
Text("Show Bottom Sheet")
}
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "Bottom Sheet Title",
style = AppTheme.typography.h2
)
Spacer(modifier = Modifier.height(16.dp))
Text("This is the content of the bottom sheet.")
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { showBottomSheet = false },
modifier = Modifier.fillMaxWidth()
) {
Text("Close")
}
}
}
Bottom Sheet with List
var showBottomSheet by remember { mutableStateOf(false) }
val items = listOf("Option 1", "Option 2", "Option 3", "Option 4")
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false }
) {
Column {
Text(
text = "Select an option",
style = AppTheme.typography.h2,
modifier = Modifier.padding(16.dp)
)
items.forEach { item ->
Surface(
onClick = {
// Handle selection
showBottomSheet = false
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = item,
modifier = Modifier.padding(16.dp)
)
}
HorizontalDivider()
}
}
}
Bottom Sheet with Controlled State
val sheetState = rememberModalBottomSheetState()
var isVisible by remember { mutableStateOf(false) }
ModalBottomSheet(
sheetState = sheetState,
isVisible = isVisible,
onDismissRequest = { isVisible = false }
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Controlled Bottom Sheet")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { isVisible = false }) {
Text("Close")
}
}
}
Bottom Sheet Without Drag Handle
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false },
dragHandle = null
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("No drag handle")
Text("Swipe down or tap outside to dismiss")
}
}
Bottom Sheet with Gestures Disabled
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false },
sheetGesturesEnabled = false
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Gestures disabled")
Text("You must use the button to close")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { showBottomSheet = false }) {
Text("Close")
}
}
}
var showBottomSheet by remember { mutableStateOf(false) }
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Contact Form",
style = AppTheme.typography.h2
)
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
TextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
// Submit form
showBottomSheet = false
},
modifier = Modifier.fillMaxWidth()
) {
Text("Submit")
}
}
}
Bottom Sheet with Scrollable Content
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false }
) {
Column {
Text(
text = "Terms and Conditions",
style = AppTheme.typography.h2,
modifier = Modifier.padding(16.dp)
)
LazyColumn(
modifier = Modifier
.weight(1f, fill = false)
.padding(horizontal = 16.dp)
) {
items(20) { index ->
Text(
text = "Section ${index + 1}: Lorem ipsum dolor sit amet...",
modifier = Modifier.padding(vertical = 8.dp)
)
}
}
HorizontalDivider()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { showBottomSheet = false },
modifier = Modifier.weight(1f)
) {
Text("Decline")
}
Button(
onClick = {
// Accept terms
showBottomSheet = false
},
modifier = Modifier.weight(1f)
) {
Text("Accept")
}
}
}
}
Bottom Sheet with Icons and Actions
data class ShareOption(
val icon: ImageVector,
val title: String,
val onClick: () -> Unit
)
val shareOptions = listOf(
ShareOption(Icons.Default.Email, "Email") { /* Share via email */ },
ShareOption(Icons.Default.Message, "Message") { /* Share via message */ },
ShareOption(Icons.Default.ContentCopy, "Copy Link") { /* Copy link */ }
)
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false }
) {
Column {
Text(
text = "Share",
style = AppTheme.typography.h2,
modifier = Modifier.padding(16.dp)
)
shareOptions.forEach { option ->
Surface(
onClick = {
option.onClick()
showBottomSheet = false
},
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = option.icon,
contentDescription = null
)
Text(option.title)
}
}
HorizontalDivider()
}
}
}
Styling
Default Styles
The bottom sheet has these default styles:
- Shape: Rounded top corners (16dp radius)
- Container Color:
AppTheme.colors.background
- Scrim Color:
AppTheme.colors.scrim
- Drag Handle: 36dp wide, 6dp high, secondary color
Custom Drag Handle
ModalBottomSheet(
isVisible = showBottomSheet,
onDismissRequest = { showBottomSheet = false },
dragHandle = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.DragHandle,
contentDescription = "Drag handle"
)
}
}
) {
// Content
}
Features
- Drag to Dismiss: Swipe down to dismiss the sheet
- Scrim Click: Tap outside to dismiss
- Animated Transitions: Smooth slide-up and slide-down animations
- Drag Handle: Visual indicator for draggable area
- Gesture Control: Enable/disable swipe gestures
- State Management: Control expansion programmatically
- Rounded Corners: Material design appearance
- Theme Integration: Uses app theme colors
Best Practices
- Content Height: Keep content concise or make it scrollable
- Drag Handle: Keep the default drag handle for better UX
- Dismiss Options: Provide both gesture and button dismissal
- Focus Management: Focus important actions within the sheet
- Loading States: Show progress indicators for async operations
- Accessibility: Provide content descriptions for actions
Common Patterns
Confirmation Dialog
ModalBottomSheet(
isVisible = showConfirmation,
onDismissRequest = { showConfirmation = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color.Red,
modifier = Modifier.size(48.dp)
)
Text(
text = "Are you sure?",
style = AppTheme.typography.h2
)
Text(
text = "This action cannot be undone.",
style = AppTheme.typography.body2
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { showConfirmation = false },
modifier = Modifier.weight(1f)
) {
Text("Cancel")
}
Button(
onClick = {
// Perform action
showConfirmation = false
},
modifier = Modifier.weight(1f)
) {
Text("Confirm")
}
}
}
}
Source Reference
ModalBottomSheet component implementation: components-lab/src/commonMain/kotlin/com/nomanr/lumo/ui/components/ModalBottomSheet.kt