Skip to content

Dialog

An unstyled Dialog component that can be used to implement Dialogs with the styling of your choice.

Fully accessible, supports animations, offers consistent behavior across platforms and an optional background scrim.

Installation

build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    implementation("com.composables:core:1.19.1")
}

Basic Example

A dialog consists of the following components: Dialog, DialogPanel and the optional Scrim.

The Dialog controls the visibility of the dialog via the DialogState object.

The DialogPanel is a container component that renders the dialog's panel and its contents.

The optional Scrim component is used to add layer behind the dialog and dim the rest of the UI.

val dialogState = rememberDialogState()

Box {
    Box(Modifier.clickable { dialogState.visible = true }) {
        BasicText("Show Dialog")
    }
    Dialog(state = dialogState) {
        DialogPanel(
            modifier = Modifier
                .displayCutoutPadding()
                .systemBarsPadding()
                .widthIn(min = 280.dp, max = 560.dp)
                .padding(20.dp)
                .clip(RoundedCornerShape(12.dp))
                .border(1.dp, Color(0xFFE4E4E4), RoundedCornerShape(12.dp))
                .background(Color.White),
        ) {
            Column {
                Column(Modifier.padding(start = 24.dp, top = 24.dp, end = 24.dp)) {
                    BasicText(
                        text = "Update Available",
                        style = TextStyle(fontWeight = FontWeight.Medium)
                    )
                    Spacer(Modifier.height(8.dp))
                    BasicText(
                        text = "A new version of the app is available. Please update to the latest version.",
                        style = TextStyle(color = Color(0xFF474747))
                    )
                }
                Spacer(Modifier.height(24.dp))
                Box(Modifier.padding(12.dp)
                    .align(Alignment.End)
                    .clip(RoundedCornerShape(4.dp))
                    .clickable(role = Role.Button) { /* TODO */ }
                    .padding(horizontal = 12.dp, vertical = 8.dp)) {
                    BasicText(
                        text = "Update",
                        style = TextStyle(color = Color(0xFF6699FF))
                    )
                }
            }
        }
    }
}

Styling

Any sort of styling is done by the Modifier of the respective component.

Changing the looks of the dialog's panel is done by passing the respective styling Modifiers to your DialogPanel:

Dialog(state = rememberDialogState(visible = true)) {
    DialogPanel(
        modifier = Modifier.systemBarsPadding()
            .widthIn(min = 280.dp, max = 560.dp)
            .padding(20.dp)
            .clip(RoundedCornerShape(12.dp))
            .border(1.dp, Color(0xFFE4E4E4), RoundedCornerShape(12.dp))
            .background(Color.White)
    ) {
        Column {
            BasicText("Something important happened")
            Box(Modifier.clickable { /* TODO */ }) {
                BasicText("Got it")
            }
        }
    }
}

Code Examples

Showing/Hide the dialog

Pass your own DialogState to the Dialog and change the visible property according to your needs:

val state = rememberDialogState()

Box(Modifier.clickable { state.visible = true }) {
    BasicText("Show dialog")
}
Dialog(state = state) {
    DialogPanel {
        Column {
            BasicText("Something important happened")
            Box(Modifier.clickable { state.visible = false }) {
                BasicText("Got it")
            }
        }
    }
}

The Dialog will also be automatically dismissed by default if the user taps outside the DialogPanel or presses the ' Escape' or 'Back' button on their device.

In override to override this behavior pass the DialogProperties object to the Dialog with the desired properties:

Dialog(
    state = rememberDialogState(),
    properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false)
) {
    // TODO the rest of your dialog
}

Adding a scrim

Use the Scrim component within your Dialog:

val state = rememberDialogState()

Box(Modifier.clickable { state.visible = true }) {
    BasicText("Show dialog")
}
Dialog(state = state) {
    Scrim()
    DialogPanel {
        Column {
            BasicText("Something important happened")
            Box(Modifier.clickable { state.visible = false }) {
                BasicText("Got it")
            }
        }
    }
}

The Scrim is customizable and you can pass any scrimColor, and enter/exit transitions that matches your design specs.

Scrollable dialogs

Add any scrollable component such as LazyColumn to the contents of your dialog:

val state = rememberDialogState()

Box(Modifier.clickable { state.visible = true }) {
    BasicText("Show dialog")
}
Dialog(state = state) {
    DialogPanel {
        Column {
            LazyColumn(Modifier.height(320.dp)) {
                item { BasicText("Something important happened") }
                repeat(100) { i ->
                    item { BasicText("Update number ${i}") }
                }
            }
            Box(Modifier.clickable { state.visible = false }) {
                BasicText("Got it")
            }
        }
    }
}

Full-screen dialogs

Pass a Modifier.fillMaxSize() to the DialogPanel's modifier parameter. Make sure to pass the Modifier.systemBarsPadding() and Modifier.displayCutoutPadding() or any related inset Modifier so that the dialog is not drawn behind any system bars (such as status and navigation bar on Android):

Dialog(state = rememberDialogState(visible = true)) {
    DialogPanel(
        modifier = Modifier
            .displayCutoutPadding()
            .systemBarsPadding()
            .fillMaxSize()
    ) {
        Column {
            BasicText("This is a full screen dialog")
            Box(Modifier.clickable { state.visible = false }) {
                BasicText("Got it")
            }
        }
    }
}

Adding transitions

Add any enter/exit transitions into the DialogPanel and Scrim to control how they appear on the screen when they enter and exit the composition:

Dialog(state = rememberDialogState(visible = true)) {
    Scrim(enter = fadeIn(), exit = fadeOut())
    DialogPanel(
        enter = scaleIn(initialScale = 0.8f) + fadeIn(tween(durationMillis = 250)),
        exit = scaleOut(targetScale = 0.6f) + fadeOut(tween(durationMillis = 150)),
    ) {
        // TODO the dialog's contents
    }
}

Styling the system bars

Android only

Dialogs will not change the color of your system UI when displayed. We provide a LocalModalWindow composition local, which provides you with the Android Window that hosts the dialog, so that you can customize the System UI according to your needs:

Dialog(rememberDialogState()) {
    DialogPanel {
        val window = LocalModalWindow.current
        LaunchedEffect(Unit) {
            // change system bars to transparent
            window.statusBarColor = Color.Transparent.toArgb()
            window.navigationBarColor = Color.Transparent.toArgb()

            // don't forget to update the icons too
            val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
            windowInsetsControllerCompat.isAppearanceLightStatusBars = true
            windowInsetsControllerCompat.isAppearanceLightNavigationBars = true
        }
        BasicText("Transparent bars. So cool 😎 ", modifier = Modifier.navigationBarsPadding())
    }
}

Keyboard Interactions

Key Description
Esc
Closes any open dialogs, if the dismissOnBackPress of DialogProperties() is set to true.
Back
Closes any open dialogs, if the dismissOnBackPress of DialogProperties() is set to true.
Tab
Cycles through the dialog's contents.
Shift + Tab
Cycles through the dialog's contents in backwards order.

Parameters

Dialog

The main component.

Parameter Description
state A DialogState object which controls the visibility of the dialog.
properties Properties that control when the dialog needs to be dismissed (such as clicking outside of the panel or pressing Esc or Back.
onDismiss Called when the dialog is being dismissed either by tapping outside or by pressing Esc or Back.
content A @Composable function that provides a DialogScope.

DialogPanel

The visual representation of your dialog. Can only be used from a DialogScope.

Parameter Description
state A DialogState object which controls the visibility of the dialog.
properties Properties that control when the dialog needs to be dismissed (such as clicking outside of the panel or pressing Esc or Back.
content A @Composable function that provides a DialogScope.

Scrim

The dimming layer that is often placed behind a DialogPanel, to let the user focus on the dialog. Can only be used from a DialogScope.

Parameter Description
modifier Modifier for the Scrim
scrimColor Controls the color of the Scrim. The default color is Black with an alpha of 60%.
enter The EnterTransition when the Scrim enters the composition
exit The ExitTransition when the Scrim enters the composition

Compose Unstyled Dialog vs Compose Dialog

Compose Multiplatform's original Dialog component does not support custom animations. Even though it is possible to animate it, it requires you to combine multiple components together and sync state to animations to composition which is not straightforward to do.

In addition, the Jetpack Compose (Android) Dialog comes with the original Android's dialog width and inset constraints, which are historically a pain to deal with and customize.

Our Dialog is designed to be customizable inside out and work the same way on every platform without surprises. It behaves like any other component. If for example you need a full screen dialog, all you have to do is pass Modifier.fillMaxSize() to the DialogPanel, without having to worry about platform flags.

Styled Examples

Looking for styled components for Jetpack Compose or Compose Multiplatform?

Explore a rich collection of production ready examples at ComposablesUi.com

Composables UI