Balloon is a free, open source design & prototyping project written in Kotlin and released under Apache-2.0. It has 4,011 GitHub stars, 310 forks and 1 open issues, and was last pushed 7 days ago. On this registry it ranks #49 of 81 tracked projects in Design & Prototyping, with 5 head-to-head comparisons available.

What is Balloon?

Balloon is an open-source Kotlin tooltip library for Compose Multiplatform that gives Android, iOS, desktop, and web developers fully customizable, animated tooltips through a single shared artifact.

What it is

Balloon is a library for building modernized and sophisticated tooltips, popups, and anchored callouts in Jetpack Compose and Compose Multiplatform. Version 2.0.0 was a full rewrite on Compose Multiplatform, so one artifact now runs on Android, iOS, Desktop (JVM), and Web (Wasm), with everything drawn by Compose rather than by a native window. The API contains no Context, no View, and no XML anywhere; instead a tooltip is described by a style, built with rememberBalloonBuilder, and controlled by a state object created with rememberBalloonState.

The concrete problem it solves is that tooltip UIs on Android were historically built on PopupWindow and the View system, which tied the code to Android and required platform plumbing that Compose Multiplatform code cannot use. Balloon replaces that View-based implementation, which remains available as version 1.7.6 and is documented separately under the legacy View guide. Because the library draws its own popup and overlay scrim, a tooltip behaves identically across the supported targets instead of needing a separate implementation per platform.

Key capabilities

  • One dependency compiles for the android, jvm (Desktop), iosArm64, iosSimulatorArm64, iosX64, and wasmJs targets.
  • The rememberBalloonBuilder DSL configures appearance with calls such as setArrowSize(10.dp), setArrowPosition(0.5f), setWidthRatio(0.7f), setPadding(12.dp), setCornerRadius(8.dp), setBackgroundColor(...), and setBalloonAnimation(BalloonAnimation.ELASTIC).
  • Two attachment styles are supported: wrapping an anchor in the Balloon composable and passing the balloon body through balloonContent, or decorating an existing composable with Modifier.balloon beneath a BalloonHost.
  • BalloonState centralizes visibility with showAlignTop(), showAlignBottom(), showAlignStart(), showAlignEnd(), showAsDropDown(), showAtCenter(...), show(BalloonAlign.BOTTOM, xOffset = ..., yOffset = ...), toggle(), dismiss(), update(BalloonAlign.TOP), dismissWithDelay(scope, 1_500L), and the observable isVisible.
  • Every show method has a suspend counterpart, such as awaitAlignTop() and awaitAtCenter(BalloonCenterAlign.END), which returns once the balloon is dismissed so tutorials can be written as sequences inside a LaunchedEffect.
  • The arrow edge is derived from the alignment used to show the balloon, so it always points back at the anchor, and update moves the balloon without replaying the animation.
  • A missing BalloonHost throws an exception that says so rather than silently rendering nothing.

Who uses it and how

  • The README reports more than 800,000 downloads every month and links to usecases.md for the list of products shipping it.
  • Multiplatform teams add com.github.skydoves:balloon to commonMain.dependencies and share one tooltip implementation across Android, iOS, desktop, and web builds.
  • Android-only teams add the same artifact to a single module's build.gradle.kts dependencies block.
  • Teams upgrading from the View implementation follow the migration guide from 1.x to 2.0.0, while those that must stay on Views keep the 1.7.6 release.
  • Application developers use the Balloon composable or Modifier.balloon with BalloonHost to attach onboarding and profile-editing hints to buttons.

Getting started

Add implementation("com.github.skydoves:balloon:2.0.1") to your module's build.gradle.kts, either in commonMain.dependencies for Compose Multiplatform or in a plain dependencies block for Android only, and documentation is published at https://skydoves.github.io/Balloon/.

How it compares

Balloon stands alone in this registry; no comparable tooltip or popup libraries are named in the available facts.

When to use it — and when not

It runs entirely in the client UI layer, so there is no database, storage, or SMTP to operate, but it does require your project to be on Compose, and version 2.0.0 is a full rewrite, so 1.x users must work through the migration guide rather than upgrading in place. Developers who need the View-based API are limited to version 1.7.6 and will not receive the 2.x Compose features, and teams standardizing on Views rather than Compose should not pick it.

project readme (upstream, from github) — read inline

Balloon


:balloon: Modernized and sophisticated tooltips for Compose Multiplatform, fully customizable with an arrow and animations.


Google Twitter LinkedIn Profile
License API Platform Build Status Medium Profile Dokka


Balloon tooltips on a profile screen Balloon tooltips in a list Balloon shown from a Compose demo

Who's using Balloon?

👉 Check out who's using Balloon

Balloon hits +800,000 downloads every month around the globe! :balloon:

globe

What's new in 2.0.0

Balloon 2.0.0 is a full rewrite on Compose Multiplatform. One artifact now runs on Android, iOS, Desktop (JVM), and Web (Wasm), and everything is drawn by Compose instead of a PopupWindow. There is no Context, no View, and no XML anywhere in the API.

If you are coming from 1.x, read the Migration guide from 1.x to 2.0.0. The View based implementation is still available at version 1.7.6, documented under Balloon 1.x (View).

💝 Sponsors

stream

Including in your project

Maven Central

Gradle

Add the dependency below to your module's build.gradle.kts file.

Compose Multiplatform

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

Android only

dependencies {
    implementation("com.github.skydoves:balloon:2.0.1")
}

Supported targets: android, jvm (Desktop), iosArm64, iosSimulatorArm64, iosX64, wasmJs.

How to Use

A balloon is made of two things: a style that describes how it looks, and a state that decides when it shows.

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

val balloonState = rememberBalloonState(style)

Then attach it to an anchor. There are two ways to do that.

Balloon composable

Wrap the anchor with the Balloon composable. The balloon 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")
    }
}

Modifier.balloon

If you would rather decorate an existing composable than wrap it, use Modifier.balloon. It needs a BalloonHost somewhere above it, which is what actually renders the popup and the overlay scrim.

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 your screen in BalloonHost once and every Modifier.balloon below it works. Forgetting it throws an exception that says so, instead of silently rendering nothing.

Showing and dismissing

BalloonState is the single place that controls visibility.

balloonState.showAlignTop()  // above the anchor
balloonState.showAlignBottom()  // below the anchor
balloonState.showAlignStart()  // leading side
balloonState.showAlignEnd()  // trailing side
balloonState.showAsDropDown()  // below, leading edges aligned
balloonState.showAtCenter(BalloonCenterAlign.TOP)
balloonState.show(BalloonAlign.BOTTOM, xOffset = 8.dp, yOffset = 4.dp)

balloonState.toggle()
balloonState.dismiss()
balloonState.update(BalloonAlign.TOP)  // move without replaying the animation
balloonState.dismissWithDelay(scope, 1_500L)

balloonState.isVisible  // observable in composition

Every show has a suspend twin that returns once the balloon is dismissed, which makes sequences easy to write.

LaunchedEffect(Unit) {
    firstBalloon.awaitAlignTop()
    secondBalloon.awaitAlignBottom()
    thirdBalloon.awaitAtCenter(BalloonCenterAlign.END)
}

Positioning

Balloon aligned above its anchor Balloon aligned below its anchor Balloon aligned to the start of its anchor Balloon aligned to the end of its anchor

The arrow edge is derived from the alignment you show with, so it always points back at the anchor without you naming it. When the requested side has no room and the opposite side does, the balloon flips over and the arrow follows it. A final clamp keeps the balloon inside the window.

To pin the arrow to a specific edge regardless of placement:

setArrowOrientation(ArrowOrientation.TOP)
setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED)

Arrow

setIsVisibleArrow(true)
setArrowSize(10.dp)  // square
setArrowSize(width = 16.dp, height = 8.dp)  // base and protrusion
setArrowPosition(0.62f)  // 0f..1f along the edge
setArrowPositionRules(ArrowPositionRules.ALIGN_ANCHOR)
setArrowColor(Color.White)

ALIGN_BALLOON reads arrowPosition as a fraction of the balloon, and ALIGN_ANCHOR reads it as a fraction of the anchor, so the arrow keeps pointing at the same spot on the anchor wherever the balloon lands. Under ALIGN_ANCHOR the arrow is kept arrowSize * arrowAlignAnchorPaddingRatio + arrowAlignAnchorPadding clear of the balloon's ends.

Size and spacing

setWidth(200.dp)  // fixed
setWidthRatio(0.6f)  // fraction of the window
setMinWidth(120.dp)
setMaxWidth(320.dp)
setMinWidthRatio(0.3f)
setMaxWidthRatio(0.9f)
setHeight(120.dp)
setSize(width = 200.dp, height = 120.dp)

setPadding(12.dp)
setPadding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp)
setPaddingHorizontal(16.dp)
setPaddingVertical(8.dp)

setMargin(12.dp)
setMarginHorizontal(16.dp)
setElevation(2.dp)

Width and height specs size the whole popup box, which is the visible card plus the margins and the elevation inset. Set setElevation(0.dp) and no margin if you want the card itself to be exactly the size you asked for.

Colors and border

Stroke

setBackgroundColor(Color(0xFF785EF0))
setArrowColor(Color.White)  // Color.Unspecified inherits the background
setCornerRadius(12.dp)
setBorder(color = Color.White, thickness = 2.dp)
setAlpha(0.9f)

The border traces the real silhouette, arrow included, at exactly the thickness you asked for.

Overlay

Overlay with an oval cut-out around the anchor Overlay with a rectangular cut-out Overlay with a circular cut-out Overlay with a rounded rectangle cut-out

An overlay dims the whole window and cuts the anchor out of it, which is how you build a spotlight tour.

setIsVisibleOverlay(true)
setOverlayColor(Color(0x99000000))
setOverlayPadding(6.dp)
setOverlayShape(BalloonOverlayShape.RoundRect(radiusX = 12.dp, radiusY = 12.dp))
setBalloonOverlayAnimation(BalloonOverlayAnimation.FADE)
setDismissWhenOverlayClicked(true)

Shapes available: Empty, Rect, Oval, Circle(radius), RoundRect(radiusX, radiusY), and RoundRectPerCorner(topStart, topEnd, bottomEnd, bottomStart).

A balloon with an overlay must sit under a BalloonHost, because a popup cannot cover the system bars. The scrim fills the host's own bounds, so put BalloonHost at the root of an edge-to-edge window with Modifier.fillMaxSize() if you want it to dim the whole screen.

Animations

Fade enter animation Overshoot enter animation Elastic enter animation Circular reveal enter animation

setBalloonAnimation(BalloonAnimation.ELASTIC)  // NONE, FADE, OVERSHOOT, ELASTIC, CIRCULAR
setCircularDuration(500L)

The durations, interpolators, and pivots are ports of the original animation resources, so the motion is identical on every platform.

Highlight animations

Heartbeat highlight animation Shake highlight animation Breath highlight animation Rotate highlight animation

A looping animation that plays while the balloon is showing, to draw the eye.

setBalloonHighlightAnimation(BalloonHighlightAnimation.HEARTBEAT, startDelayMillis = 300L)

NONE, HEARTBEAT, SHAKE, BREATH, and ROTATE. ROTATE takes its parameters from setBalloonRotationAnimation(BalloonRotateAnimation(turns = 2, speedMillis = 1200)).

Listeners

Listeners are properties on the state rather than builder options, because BalloonStyle is value equal data and lambdas would break that.

balloonState.onBalloonClick = { /* the body was tapped */ }
balloonState.onOverlayClick = { /* the scrim was tapped */ }
balloonState.onDismiss = { /* the balloon closed */ }

Behavior

setDismissWhenClicked(true)
setDismissWhenTouchOutside(true)
setDismissWhenBackPressed(true)
setDismissWhenShowAgain(true)
setAutoDismissDuration(2_000L)
setFocusable(true)

Custom content

Balloon with fully custom Compose content

There is no TextForm, no IconForm, and no setLayout. The balloon body is a Compose slot, so you build it the same way you build anything else.

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() })
}

Documentation

For a full reference of every option, see the documentation.

Find this library useful? :heart:

Support it by joining stargazers for this repository. :star:
Also, follow me on GitHub for my next creations! 🤩

License

Designed and developed by 2019 skydoves (Jaewoong Eum)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Frequently asked questions

Is Balloon free to use?

Balloon is open source under the Apache-2.0 licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does Balloon do?

:balloon: Modernized and sophisticated tooltips, fully customizable with an arrow and animations for Compose and Kotlin Multiplatform.

What is Balloon written in?

Balloon is primarily written in Kotlin. Its source is publicly available at https://github.com/skydoves/Balloon, and it has 4,011 GitHub stars.