ModalBottomSheet, Snackbar, and User Feedback Patterns

Quick answer: use ModalBottomSheet when a user needs to choose, review, or act on secondary content without leaving the current screen. Use Snackbar for short, non-blocking feedback such as “Saved” or “Deleted,” especially when it offers an Undo action. Keep both visibility and events outside the component, and remove a hidden sheet from composition.

Bottom sheets, dialogs, and snackbars all appear above a screen, but their interruption level is different. A sheet can hold focused secondary UI; a snackbar should report an event while allowing the user to continue working; a dialog should reserve its stronger interruption for a decision that must be addressed.

Pick feedback by the user’s next action

SituationUseReason
Choose a filter, share target, or item actionsModalBottomSheetThe user needs focused secondary content and can dismiss it to return to the screen.
Confirm a save, report offline state, or offer UndoSnackbarIt gives brief feedback without blocking the current task.
Confirm destructive action before it happensAlertDialogIt asks for an explicit decision before proceeding.
Long or multi-step editing workflowA dedicated screenIt needs navigation, space, and reliable state restoration.

The Compose bottom-sheet guide describes a modal sheet as bottom-anchored secondary content. The Snackbar guide positions snackbars as brief feedback that disappears after a short time.

Show and remove a ModalBottomSheet

Use a Boolean to decide whether the sheet exists, and a SheetState for its visible state. show() and hide() are suspending functions, so call them from a coroutine in response to an event.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

@Composable
fun FilterSheetExample() {
    var showSheet by remember { mutableStateOf(false) }
    val sheetState = rememberModalBottomSheetState()
    val scope = rememberCoroutineScope()

    Button(onClick = { showSheet = true }) {
        Text("Filter")
    }

    if (showSheet) {
        ModalBottomSheet(
            sheetState = sheetState,
            onDismissRequest = { showSheet = false },
        ) {
            Column(Modifier.padding(24.dp)) {
                Text("Filter results")
                Button(onClick = {
                    scope.launch {
                        sheetState.hide()
                    }.invokeOnCompletion {
                        if (!sheetState.isVisible) showSheet = false
                    }
                }) {
                    Text("Apply filters")
                }
            }
        }
    }
}

The important final step is removing the sheet from composition after it hides. The official guide calls this out directly. Hiding the SheetState alone does not make the UI state disappear.

Set skipPartiallyExpanded = false only when a partial first state improves the task. The partial-sheet API is currently experimental, so use its opt-in guidance from the official partial-sheet documentation and test drag, back, and outside-tap dismissal.

Put snackbars in a Scaffold

SnackbarHostState owns the currently displayed snackbar. It is normally remembered at the screen shell and supplied to Scaffold:

import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember

@Composable
fun NotesScreen() {
    val snackbarHostState = remember { SnackbarHostState() }

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) },
    ) { innerPadding ->
        // Apply innerPadding to the screen content.
    }
}

Show a snackbar from an event handler or from an effect that handles a one-time UI event. Do not call showSnackbar() directly in the body of a composable: it is a suspending side effect and composition may run more than once.

Implement undo with SnackbarResult

An undo flow needs both the original operation and an explicit result check:

import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

val scope = rememberCoroutineScope()

fun deleteNoteWithUndo(noteId: String) {
    scope.launch {
        deleteNote(noteId)

        val result = snackbarHostState.showSnackbar(
            message = "Note deleted",
            actionLabel = "Undo",
            duration = SnackbarDuration.Long,
        )

        if (result == SnackbarResult.ActionPerformed) {
            restoreNote(noteId)
        }
    }
}

In real code, deleteNote and restoreNote belong to a state holder or repository-backed event flow, not a UI helper. A snackbar should react to a completed event; it should not be the source of truth for whether data exists. State Hoisting in Jetpack Compose covers that boundary.

Common mistakes

Using a snackbar for required input

Snackbars are short and non-blocking. Use a sheet, dialog, or screen when the user must choose an option or enter information.

Treating a modal sheet as permanent navigation

A modal sheet is secondary, dismissible content. Use NavigationBar, NavigationRail, or the adaptive navigation suite for persistent top-level destinations. BottomAppBar and NavigationRail covers that distinction.

Forgetting accessibility and system dismissal

Give sheet actions clear labels, test the drag handle and back behavior, and ensure the sheet can be dismissed through the expected paths. Keep snackbar messages concise; the action label must state what it does.

Showing every event as a snackbar

Repeated snackbars compete for attention. Reserve them for events where feedback or a quick recovery action helps the user. Inline validation belongs near the field; high-risk confirmation belongs in a dialog.

Test the feedback flow

  • Confirm a hidden sheet is removed from composition after programmatic hide and user dismissal.
  • Test a partially expanded sheet only if the task benefits from it.
  • Verify the snackbar does not cover an important action or content at system-bar boundaries.
  • Test Undo after deletion and the normal dismissal path without Undo.
  • Check TalkBack, large font scale, and long localized action labels.

A practical rule

Use ModalBottomSheet for an optional focused task, Snackbar for brief status and recovery, and AlertDialog for a required decision. This keeps feedback proportional to the user’s next action instead of turning every event into a modal interruption.