Dialogs in Jetpack Compose: AlertDialog and Custom Dialogs

Quick answer: use Material 3
AlertDialogfor a focused decision with standard title, message, and action slots. Use the lower-levelDialogwhen the content needs a custom form, layout, or interaction pattern. Keep dialog visibility in the screen state, and make every dismiss path update that state.
Dialogs interrupt the current task. They are appropriate for a decision that needs attention now—such as confirming deletion—not for routine navigation, long reading, or a transient success message.
The official Compose dialog guide describes AlertDialog as the themed shortcut and Dialog as the unstyled container for genuinely custom content.
Choose AlertDialog or Dialog
| Need | Use | Why |
|---|---|---|
| Confirm, discard, sign out, or acknowledge a short message | AlertDialog | Its title, text, and button slots already follow Material dialog structure. |
| A custom form, image treatment, multi-step content, or unusual layout | Dialog with an inner Card or Surface | You own the size, shape, container, and internal layout. |
| A date or time choice | Material date/time picker dialog | It supplies task-specific interaction and semantics. |
| A non-blocking success, error, or undo message | Snackbar or other feedback pattern | A modal dialog adds unnecessary interruption. |
Do not build a custom dialog just to reproduce a title, body, cancel button, and confirm button. That duplicates Material behavior and makes it easier to forget a dismissal or accessibility detail.
Hoist the visibility state
The screen decides whether a dialog exists; the dialog receives callbacks for dismissal and confirmation. This keeps navigation, business work, and UI rendering separate.
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun DeleteNoteEntry(onDelete: () -> Unit) {
var showDialog by remember { mutableStateOf(false) }
Button(onClick = { showDialog = true }) {
Text("Delete note")
}
if (showDialog) {
DeleteNoteDialog(
onDismiss = { showDialog = false },
onConfirm = {
showDialog = false
onDelete()
},
)
}
}For a screen that can be recreated while a dialog is visible, decide whether restored visibility is meaningful. Use rememberSaveable only when reopening the prompt is genuinely correct; do not restore a one-time destructive confirmation that no longer matches current state. State Hoisting in Jetpack Compose explains the broader ownership pattern.
Create a focused confirmation with AlertDialog
AlertDialog gives you explicit slots for the icon, title, explanatory text, dismiss action, and confirm action:
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@Composable
fun DeleteNoteDialog(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Delete this note?") },
text = { Text("This permanently removes the note from this device.") },
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Delete")
}
},
)
}onDismissRequest is called for dismissal such as tapping outside the dialog or using the system back action. It does not remove the dialog automatically: your callback must change the state so the dialog leaves composition. The confirm and dismiss buttons should do the same after completing their intended action.
Use specific action labels. “Delete” communicates the consequence better than “OK,” and “Cancel” keeps the safe path clear. A destructive operation can require an additional product-specific safeguard, but never rely on red color alone to explain the consequence.
Build custom content with Dialog
Dialog is a window container without Material content structure. Add a Card or Surface inside it; otherwise child content can appear directly over the dimmed background with no visual boundary.
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.unit.dp
@Composable
fun RenameFolderDialog(
name: String,
onDismiss: () -> Unit,
onSave: () -> Unit,
) {
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
) {
Column(Modifier.padding(24.dp)) {
Text("Rename folder", style = MaterialTheme.typography.titleLarge)
// Put a state-hoisted text field here.
Text("Current name: $name")
Button(onClick = onSave, modifier = Modifier.padding(top = 16.dp)) {
Text("Save")
}
}
}
}
}This sample intentionally leaves the text-field state outside the dialog. A dialog that edits data should receive the current value and emit events; the screen or ViewModel validates and persists it. For the field and keyboard details, see TextField in Jetpack Compose and Form Validation.
Use DialogProperties only for a real behavior requirement, such as disabling outside-click dismissal during an irreversible in-progress operation. If dismissal is disabled, provide a visible and working cancel or close path unless the task genuinely cannot be abandoned.
Avoid dialog mistakes
Leaving a dismissed dialog in composition
Calling a callback without changing showDialog leaves the dialog visible. Route every outside tap, back action, cancel button, and confirm path to the right state transition.
Putting long, scrollable workflows in an alert dialog
An alert dialog is for a compact interruption. Move extended forms to a screen or a bottom sheet when users need space, context, or multiple steps.
Customizing away contrast and focus
For a custom dialog, use MaterialTheme colors, shapes, and typography rather than hard-coded surfaces. Keep actions reachable with touch and keyboard, and test the dialog with TalkBack and large font scale. ColorScheme in Material 3 covers safe semantic colors for custom surfaces.
Reusing a dialog for transient feedback
A completed save, network retry, or undo opportunity is usually better served by a snackbar. Dialogs should request a decision, not announce every state change.
Test the whole modal flow
- Open the dialog from the real action and verify focus moves into it.
- Test outside tap and system back behavior when dismissal is allowed.
- Verify cancel changes nothing and confirm invokes exactly one event.
- Test the custom content at large font scale, with long localized text, and with the IME open.
- Check that a destructive confirmation remains understandable without color or icon alone.
A practical rule
Use AlertDialog by default for concise decisions. Use Dialog when custom content is the actual requirement, not merely an opportunity to redraw an alert. In both cases, hoist visibility and data state, close the dialog through every dismissal path, and keep the interruption focused on one user decision.