skydoves/Balloon: Compose Multiplatform Tooltips Without PopupWindow
:balloon: Modernized and sophisticated tooltips, fully customizable with an arrow and animations for Compose and Kotlin Multiplatform.
At a glance
- What is it?
- Balloon 2.0.1 is a Kotlin tooltip library that draws its popups entirely in Compose and targets Android, iOS, Desktop and Wasm. It is a good fit when you already live in Compose, and the wrong one when your UI is still View and XML based.
- Who is it for?
- Adopt Balloon 2.0.1 if your UI is Compose Multiplatform or Compose for Android and you want tooltips with an arrow, animations, and a single state object controlling visibility. Do not adopt it if your screens are still View and XML based: stay on 1.7.6, which the README documents under the legacy View docs, or keep your current popup code.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem Balloon solves, and for whom
Tooltips are one of those UI elements that look trivial until you need an arrow that points at the right pixel, an animation that does not fight the rest of the screen, and a dismissal rule that behaves the same on every platform. On Android the classic answer was a PopupWindow plus a custom layout, which means XML, a Context, and a separate code path for every screen density. Balloon 2.0.0 replaces that with a Compose-first API. The README states that 2.0.0 is a full rewrite on Compose Multiplatform, that one artifact now runs on Android, iOS, Desktop (JVM) and Web (Wasm), and that everything is drawn by Compose instead of a PopupWindow. It also states plainly that there is no Context, no View, and no XML anywhere in the API.
That last sentence defines the audience. Balloon is for teams whose screens are already composables and who want the tooltip to be part of the composition rather than a window floating above it. A Kotlin Multiplatform project sharing UI across Android and iOS gets the same tooltip implementation on both, which is the main appeal. An Android-only Compose app gets a smaller benefit, mostly in API ergonomics. A team still on XML layouts and Fragments gets nothing from 2.0.1 and should not start here.
Style, state, anchor: the three pieces of a Balloon
The library splits a tooltip into two objects and one attachment point. The style describes how it looks. The state decides when it shows. The anchor is whatever composable the tooltip points at.
The README gives the style as a rememberBalloonBuilder block, where you set the arrow size, the arrow position as a fraction, the width ratio, padding, corner radius, background color and the animation. The state comes from rememberBalloonState(style). From there you attach the pair to an anchor in one of two ways. The Balloon composable wraps the anchor, with the tooltip body in balloonContent and the anchor in the trailing lambda. Modifier.balloon decorates an existing composable instead, and the README notes that it requires a BalloonHost somewhere above it, because that host is what renders the popup and the overlay scrim. The README is explicit that forgetting the host throws an exception saying so, rather than silently rendering nothing. That is a deliberate design choice worth noting: a silent failure here would be far harder to debug than a crash.
Visibility lives entirely in BalloonState. The README lists showAlignTop, showAlignBottom, showAlignStart, showAlignEnd, showAsDropDown, showAtCenter with a BalloonCenterAlign, and a general show that takes a BalloonAlign plus xOffset and yOffset. There is also toggle, dismiss, update to move the balloon without replaying the animation, and dismissWithDelay taking a scope and a millisecond value. isVisible is described as observable in composition. The README also says every show has a suspend twin that returns once the balloon is dismissed.
Installing Balloon 2.0.1 and showing a first tooltip
Balloon is published on Maven Central under the com.github.skydoves group. The README gives two Gradle snippets, one for Compose Multiplatform and one for Android only. For a multiplatform module, the dependency goes in commonMain inside the kotlin sourceSets block. The version in the README is 2.0.1.
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.github.skydoves:balloon:2.0.1")
}
}
}For an Android-only module, the README uses the plain dependencies block instead.
dependencies {
implementation("com.github.skydoves:balloon:2.0.1")
}The README lists the supported targets as android, jvm (Desktop), iosArm64, iosSimulatorArm64, iosX64 and wasmJs, and the badge in the repository header states API 23+ for Android.
A first tooltip needs a style and a state, then an anchor. The README's own example builds the style with setArrowSize, setArrowPosition, setWidthRatio, setPadding, setCornerRadius, setBackgroundColor and setBalloonAnimation.
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 wrap the anchor with the Balloon composable and call a show method from the anchor's click handler. In the README's example the button calls balloonState.showAlignTop(), and the tooltip body is a Text inside balloonContent.
Balloon(
state = balloonState,
balloonContent = {
Text(
text = "Now you can edit your profile!",
color = Color.White,
)
},
) {
Button(onClick = { balloonState.showAlignTop() }) {
Text(text = "Edit profile")
}
}If you prefer decorating over wrapping, the README shows Modifier.balloon on the button's modifier, with the tooltip content in a trailing lambda, inside a BalloonHost that wraps the surrounding column. The reader should expect the tooltip to appear above the button on click, animated, with the arrow centered at 0.5 of the balloon's width.
Where Balloon 2.0.1 is the wrong choice
The migration from 1.x is the first real cost. The README directs anyone coming from 1.x to a migration guide at docs/migration.md, and states that the View based implementation is still available at version 1.7.6, documented under the legacy View docs. So a project that cannot move to Compose is not abandoned, but it is frozen on a line that predates the rewrite. The README does not document an end-of-support date for 1.7.6, and it does not describe a compatibility shim that would let 2.0.1 render into a View hierarchy. If your app is a mixed Compose and XML codebase, that boundary is where the work lives.
The second constraint is the BalloonHost requirement for Modifier.balloon. The README frames the thrown exception as a feature, and it is better than silence, but it also means the modifier is not drop-in: you have to place a host at the right level of your tree, and a host placed too low will not cover the screens that need it. The README does not describe how multiple hosts interact, or what happens if two are nested.
The third is scope. This is a tooltip library, not an onboarding or coach-mark framework. The README documents positioning, styling, animation and dismissal. It does not document sequencing a tour across several anchors, persisting which tips a user has already seen, or gating display on first launch. Those are the parts teams usually end up writing themselves, and the README is silent on them.
Balloon compared with a plain Compose Popup
The obvious alternative is Compose's own Popup, or a hand-rolled overlay with a Box and a scrim. The difference is not that Balloon can do something Popup cannot. It is that Popup gives you a positioned surface and leaves the rest to you: the arrow, the animation, the alignment helpers, the delay before dismissal. Balloon packages those as configuration. setArrowSize, setArrowPosition, setWidthRatio and setBalloonAnimation exist because every team that builds tooltips by hand rebuilds them.
The trade-off runs the other way too. Compose's Popup has no opinion about how your tooltip looks, so there is nothing to migrate when the library's API changes, and nothing to learn beyond the platform. Balloon 2.0.0 was a breaking rewrite of a View based API, which is exactly the kind of change a plain Popup never forces on you. If you need one tooltip in one screen, the library is more surface area than the problem. If you need consistent tooltips across a multiplatform app, the alignment and animation helpers are the reason to take the dependency.
Maintenance, licence and the cost of upgrading
The repository is not archived. The last push was on 2026-09-16, and the most recent release in the list is 2.0.1 on 2026-09-13, following 2.0.0 on 2026-08-29 and 1.7.6 on 2026-04-16. The gap between 1.7.6 and 2.0.0 is roughly four months, and the two 2.x releases landed two weeks apart, which is consistent with a rewrite settling down rather than a dormant project. The repository also carries a renovate.json and a .coderabbit.yaml at the top level, plus a benchmark module and separate androidApp, desktopApp, iosApp and wasmApp entries, so the multiplatform targets appear to be exercised by real app modules rather than only declared in Gradle.
Balloon is Apache-2.0. That is a permissive licence, and the repository ships the LICENSE file at the root. The practical implication for most teams is that you can use it in closed-source apps and modify it, provided you keep the notice and state changes. This is a general description of the licence, not legal advice, and the migration guide is the document to read before planning an upgrade rather than the licence file.
Upgrade cost concentrates in one event: the 1.x to 2.0.0 move. Once you are on 2.0.1, the API is Compose-native, and a future minor release is unlikely to carry the same weight. The README does not describe a deprecation path for the View based API beyond pointing at 1.7.6, so the decision to migrate is a decision you make once and cannot half-make.
Editorial conclusion
Adopt Balloon 2.0.1 if your UI is Compose Multiplatform or Compose for Android and you want tooltips with an arrow, animations, and a single state object controlling visibility. Do not adopt it if your screens are still View and XML based: stay on 1.7.6, which the README documents under the legacy View docs, or keep your current popup code. Before committing, verify the migration guide against your own call sites, confirm that your target set is covered by android, jvm, iosArm64, iosSimulatorArm64, iosX64 and wasmJs, and check that a BalloonHost exists above every Modifier.balloon you plan to write.
Frequently asked questions
Which platforms does skydoves/Balloon 2.0.1 support?
The README lists the supported targets as android, jvm for Desktop, iosArm64, iosSimulatorArm64, iosX64 and wasmJs. The repository header badge states API 23+ for Android.
Does skydoves/Balloon 2.0.1 still use PopupWindow or XML layouts?
No. The README states that 2.0.0 is a full rewrite on Compose Multiplatform, that everything is drawn by Compose instead of a PopupWindow, and that there is no Context, no View and no XML anywhere in the API.
How do I install skydoves/Balloon in a Compose Multiplatform project?
Add com.github.skydoves:balloon:2.0.1 to the commonMain dependencies inside the kotlin sourceSets block, as the README shows. Android-only projects use the same coordinate in a plain dependencies block.
Why does Modifier.balloon throw an exception in skydoves/Balloon?
Modifier.balloon needs a BalloonHost somewhere above it, because the host renders the popup and the overlay scrim. The README says forgetting it throws an exception that says so, instead of silently rendering nothing.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/skydoves-balloon)