Skip to content

Customization

Colors

setBackgroundColor(Color(0xFF785EF0))
setArrowColor(Color.White)  // Color.Unspecified inherits the background
setAlpha(0.9f)

setAlpha applies to the whole balloon in one layer, so overlapping parts do not double blend.

Border

Stroke

setBorder(color = Color.White, thickness = 2.dp)
setBalloonStroke(color = Color.White, thickness = 2.dp)  // same thing, 1.x name

The stroke traces the real silhouette, arrow included, at exactly the thickness you asked for. Leave borderColor at Color.Unspecified or borderThickness at 0.dp to disable it.

Content

The balloon body is a Compose slot, so there is nothing to configure on the builder. Build it with the same composables you use everywhere else.

Balloon example

Balloon(
    state = balloonState,
    balloonContent = {
        Column {
            Text(
                text = "Choose a drink",
                color = Color.White,
                style = MaterialTheme.typography.titleSmall,
            )
            Spacer(modifier = Modifier.height(8.dp))
            drinks.forEach { drink ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .clickable { onDrinkSelected(drink) }
                        .padding(vertical = 6.dp),
                    verticalAlignment = Alignment.CenterVertically,
                ) {
                    Icon(painter = painterResource(drink.icon), contentDescription = null)
                    Spacer(modifier = Modifier.width(8.dp))
                    Text(text = drink.name, color = Color.White)
                }
            }
        }
    },
) {
    Button(onClick = { balloonState.showAlignBottom() }) { Text(text = "Drinks") }
}

Interactive content works normally. A tap that a child consumes never reaches the balloon's own tap handler, so a clickable row inside the body does not accidentally trigger setDismissWhenClicked.

Reusing a style

BalloonStyle is immutable, and derive makes a variant that changes only what you name:

val base = rememberBalloonBuilder {
    setCornerRadius(8.dp)
    setPadding(12.dp)
    setBackgroundColor(Color(0xFF785EF0))
}

val warning = base.derive { setBackgroundColor(Color(0xFFFF6F00)) }
val subtle = base.derive {
    setAlpha(0.85f)
    setArrowSize(8.dp)
}

derive takes the same builder block as rememberBalloonBuilder, starting from an existing style instead of the defaults. It exists instead of a data class copy because a generated copy would name all 43 properties in the published binary interface and make adding a 44th option a breaking change.

DefaultBalloonStyle is the same value the builder produces with no calls, which makes a convenient starting point.

Restyling a visible balloon

rememberBalloonState re-applies the style on every recomposition, so an animated style updates a balloon that is already showing without hiding it:

val color by animateColorAsState(if (selected) Color(0xFF785EF0) else Color(0xFF444444))
val balloonState = rememberBalloonState(style.derive { setBackgroundColor(color) })

Behavior

setDismissWhenClicked(true)  // tapping the body closes it
setDismissWhenTouchOutside(true)  // tapping outside closes it
setDismissWhenBackPressed(true)  // back or Escape closes it
setDismissWhenShowAgain(true)  // showing a visible balloon closes it instead
setDismissWhenTouchMargin(true)  // a tap in the margin band closes it too
setDismissWhenOverlayClicked(true)  // tapping the scrim closes it
setAutoDismissDuration(2_000L)  // 0L disables
setFocusable(true)

Two of these carry side effects, kept from 1.x so ported call sites behave the same:

  • setDismissWhenTouchOutside(false) also clears focusability, so a balloon that ignores outside taps does not sit there swallowing them
  • setBalloonAnimation(BalloonAnimation.CIRCULAR) also clears focusability, so the reveal can play without the popup stealing input

Call setFocusable(true) after either one if you want focus back.

Accessibility

Balloon content is marked with an unmergeable semantics property, so a screen reader treats it as its own subtree instead of folding it into a clickable ancestor. Give the content itself meaningful semantics the way you normally would:

balloonContent = {
    Text(
        modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
        text = "Now you can edit your profile!",
    )
}