Compose Unstyled 2.0 is out! Check the official announcement blog ->

A modal dialog component with dismiss behavior and panel transitions.

import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composeunstyled.DialogPanel
import com.composeunstyled.Scrim
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledDialog
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds

@Composable
fun DialogDemo() {
  var dialogVisible by remember { mutableStateOf(true) }

  LaunchedEffect(dialogVisible) {
    if (dialogVisible.not()) {
      delay(1.seconds)
      dialogVisible = true
    }
  }

  UnstyledDialog(
    visible = dialogVisible,
    onDismissRequest = { dialogVisible = false },
    overlay = {
      Scrim(scrimColor = Color.Black.copy(0.3f), enter = fadeIn(), exit = fadeOut())
    },
  ) {
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center,
    ) {
      DialogPanel(
        modifier = Modifier
          .padding(20.dp)
          .displayCutoutPadding()
          .systemBarsPadding()
          .widthIn(max = 560.dp)
          .padding(20.dp)
          .clip(RoundedCornerShape(12.dp))
          .background(Color(0xFFF8FAFC))
          .border(1.dp, Color(0xFFCACACA), RoundedCornerShape(12.dp)),
        paneTitle = "Dialog",
        enter = scaleIn(initialScale = 0.8f) + fadeIn(tween(durationMillis = 250)),
        exit = scaleOut(targetScale = 0.6f) + fadeOut(tween(durationMillis = 150)),
      ) {
        Column {
          Column(Modifier.padding(start = 24.dp, top = 24.dp, end = 24.dp)) {
            BasicText(
              text = "Update Available",
              style = TextStyle(
                color = Color(0xFF1A1A1A),
                fontSize = 16.sp,
                lineHeight = 24.sp,
                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(0xFF1A1A1A)),
            )
          }
          Spacer(Modifier.height(24.dp))
          UnstyledButton(
            onClick = { /* TODO */ },
            modifier = Modifier
              .padding(12.dp)
              .align(Alignment.End)
              .clip(RoundedCornerShape(6.dp)),
            indication = LocalIndication.current,
          ) {
            BasicText(
              "Update",
              modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
              style = TextStyle(color = Color.Black),
            )
          }
        }
      }
    }
  }
}

Features

  • Modal focus behavior
  • Outside-click dismiss
  • Back and Escape dismiss
  • Panel enter and exit transitions

Installation

implementation("com.composables:composeunstyled-dialog")

Anatomy

UnstyledDialog(
  visible = visible,
  onDismissRequest = { visible = false },
) {
  DialogPanel {
  }
}

Concepts

  • UnstyledDialog renders dialog content in a modal layer.
  • DialogPanel renders the focusable dialog content.

Accessibility

Use the paneTitle parameter on DialogPanel when the dialog has a clear title.

Code Examples

Showing and hiding a dialog

Use the visible parameter to show the dialog and the onDismissRequest callback to update that state:

var visible by remember { mutableStateOf(false) }

BasicText(
  text = "Show dialog",
  modifier = Modifier.clickable { visible = true },
)

UnstyledDialog(
  visible = visible,
  onDismissRequest = { visible = false },
) {
  DialogPanel {
    BasicText("Dialog content")
  }
}

Adding an overlay behind a dialog

Use the overlay parameter to render content behind the dialog panel. Scrim provides a ready-made overlay for dialogs.

UnstyledDialog(
  visible = visible,
  onDismissRequest = { visible = false },
  overlay = { Scrim() },
) {
  DialogPanel {
    BasicText("Dialog content")
  }
}

Changing dismiss behavior

Use the properties parameter to control how the dialog can be dismissed:

UnstyledDialog(
  visible = visible,
  onDismissRequest = { visible = false },
  properties = DialogProperties(
    dismissOnBackPress = false,
    dismissOnClickOutside = false,
  ),
) {
  DialogPanel {
    BasicText("Dialog content")
  }
}

Animating the dialog panel

Use the enter and exit parameters on DialogPanel to animate the dialog panel:

UnstyledDialog(
  visible = visible,
  onDismissRequest = { visible = false },
) {
  DialogPanel(
    enter = fadeIn(),
    exit = fadeOut(),
  ) {
    BasicText("Dialog content")
  }
}

API Reference

UnstyledDialog

Parameter Type Description
visible Boolean
onDismissRequest () -> Unit
properties DialogProperties Properties that control when the dialog needs to be dismissed (such as clicking outside of the panel or pressing Esc or Back.
overlay (DialogOverlayScope.() -> Unit)?
content DialogScope.() -> Unit A @Composable function that provides a DialogScope.

DialogOverlayScope.Scrim

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

DialogScope.DialogPanel

Parameter Type Description
modifier Modifier Modifier for the Scrim
paneTitle String?
enter EnterTransition The EnterTransition when the Scrim enters the composition
exit ExitTransition The ExitTransition when the Scrim enters the composition
content () -> Unit A @Composable function that provides a DialogScope.