Skip to content

Getting Started

This guide covers installing Balloon 2.0.0 and attaching your first tooltip.

Installation

Maven Central

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.github.skydoves:balloon:2.0.0")
        }
    }
}
dependencies {
    implementation("com.github.skydoves:balloon:2.0.0")
}

Balloon depends on Compose Multiplatform 1.10.x and nothing else. The minimum Android SDK is 23.

The two pieces

Every balloon is made of a style and a state.

BalloonStyle is immutable value data that describes how the balloon looks. Build it with the fluent Balloon.Builder, usually through rememberBalloonBuilder:

val style = rememberBalloonBuilder {
    setArrowSize(10.dp)
    setArrowPosition(0.5f)
    setPadding(12.dp)
    setCornerRadius(8.dp)
    setBackgroundColor(Color(0xFF785EF0))
    setBalloonAnimation(BalloonAnimation.ELASTIC)
}

The builder also works outside composition, which is handy when the style is a constant:

val style = Balloon.Builder().apply {
    setArrowSize(10.dp)
    setPadding(12.dp)
    setCornerRadius(8.dp)
    setBackgroundColor(Color(0xFF785EF0))
    setBalloonAnimation(BalloonAnimation.ELASTIC)
}.build()

BalloonStyle has no public constructor on purpose. It carries 43 properties, and a constructor that names all of them would freeze every one of them into the published binary interface, so adding a 44th option later would break code compiled against 2.0.0. The builder is the one entry point and its signature never has to change.

BalloonState decides when the balloon shows and where:

val balloonState = rememberBalloonState(style)

The state survives recomposition. Passing an updated style restyles a visible balloon in place without hiding it, so you can animate colors or sizes freely.

Attaching a balloon

Option 1: the Balloon composable

Wrap the anchor. The body goes in balloonContent and the anchor goes in the trailing lambda.

Balloon(
    state = balloonState,
    balloonContent = {
        Text(text = "Now you can edit your profile!", color = Color.White)
    },
) {
    Button(onClick = { balloonState.showAlignTop() }) {
        Text(text = "Edit profile")
    }
}

Layout modifiers that belong to the anchor go on the Balloon composable itself, because it wraps the anchor in a Box:

Balloon(
    modifier = Modifier.weight(1f),
    state = balloonState,
    balloonContent = { Text(text = "Tooltip") },
) {
    Text(text = "Anchor")
}

Option 2: Modifier.balloon

When you would rather decorate an existing composable than wrap it, use Modifier.balloon. A Compose modifier cannot emit content, so the balloon is rendered by a BalloonHost that sits above the anchor in the tree.

BalloonHost {
    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Button(
            modifier = Modifier.balloon(balloonState) {
                Text(text = "Now you can edit your profile!", color = Color.White)
            },
            onClick = { balloonState.showAlignTop() },
        ) {
            Text(text = "Edit profile")
        }
    }
}

Wrap a screen in BalloonHost once and every Modifier.balloon below it works. If you forget, the modifier throws an IllegalStateException naming BalloonHost rather than silently rendering nothing.

The balloon body is composed inside the host, but it reads the CompositionLocals that were in scope at the Modifier.balloon call site, so your MaterialTheme, LocalContentColor, and LocalLayoutDirection are the anchor's, not the host's.

When do I need BalloonHost?

Always for Modifier.balloon, and for any balloon that turns on an overlay. The overlay scrim is drawn by the host so it can cover the entire window including the system bars, which a popup window cannot do.

Showing and dismissing

balloonState.showAlignTop()
balloonState.dismiss()
balloonState.toggle()

See Showing a Balloon for every placement option, offsets, auto dismiss, and coroutine sequences.

Custom content

There is no TextForm, no IconForm, and no custom layout resource. The body is a Compose slot:

Balloon(
    state = balloonState,
    balloonContent = {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Icon(imageVector = Icons.Default.Edit, contentDescription = null, tint = Color.White)
            Spacer(modifier = Modifier.width(8.dp))
            Text(text = "Edit your profile", color = Color.White)
        }
    },
) {
    ProfileImage(onClick = { balloonState.showAlignBottom() })
}