Jetpack Compose Tooltip

Every mobile app eventually needs a small way to explain what an icon or button does without cluttering the screen, and that is exactly the problem a Jetpack Compose tooltip solves. A Jetpack Compose tooltip is a small popup of text that appears near a piece of UI when a user taps and holds, hovers, or long-presses it. In Jetpack Compose, the Material 3 library ships a dedicated Jetpack Compose tooltip box composable that makes adding this behavior straightforward, so you don't have to build the popup, positioning, or dismiss logic yourself.

This guide walks through every part of the tooltip box composable in Jetpack Compose: the two Jetpack Compose tooltip styles it supports, how to control when a tooltip shows or hides, how it gets positioned on screen, and the smaller parameters that shape its behavior. By the end you will be able to add a working Jetpack Compose tooltip to any anchor in your UI.

What Is a Tooltip Box in Jetpack Compose

The TooltipBox composable is the container that wraps whatever UI element you want to attach a Jetpack Compose tooltip to. You give it two lambdas: one that defines the content of the Jetpack Compose tooltip itself, and one that defines the anchor content, meaning the actual button, icon, or text the user interacts with. Jetpack Compose then takes care of showing the tooltip near the anchor whenever it should appear.

A tooltip box in Jetpack Compose does not render anything extra by default. It just watches user interaction on the anchor content and decides, based on a state object, whether the tooltip content should be visible. This separation is what makes the Jetpack Compose tooltip box so flexible, because you can put literally any composable inside either lambda.

Here is a minimal tooltip box in Jetpack Compose wrapping an icon button:

kotlin
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BasicTooltipBoxExample() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
        tooltip = {
            PlainTooltip {
                Text("Show more information")
            }
        },
        state = rememberTooltipState()
    ) {
        IconButton(onClick = { }, modifier = Modifier.padding(8.dp)) {
            Icon(Icons.Filled.Info, contentDescription = "Info")
        }
    }
}

When this code runs, the icon button is drawn normally, but the Jetpack Compose tooltip lambda stays invisible until the state object flips to a shown status. Long-pressing the icon triggers the Jetpack Compose tooltip box to display the text inside the PlainTooltip. Notice the tooltip parameter itself is literally named tooltip, which is the slot every Jetpack Compose tooltip is built from.

You can read the full parameter list for the tooltip box composable that Jetpack Compose provides in the official Jetpack Compose Material 3 reference.

Adding a Plain Tooltip in Jetpack Compose

The PlainTooltip composable is the simplest Jetpack Compose tooltip content you can put inside a tooltip box. A Jetpack Compose plain tooltip is meant for short, single-line hints, such as labeling an icon or explaining an abbreviation. It renders as a small rounded surface with a text label and nothing else.

A Jetpack Compose plain tooltip accepts a modifier, an optional caret drawn toward the anchor, a container color, a content color, and a shape, but most of the time you will only pass the text content. Because a Jetpack Compose plain tooltip is meant to stay short, it generally does not include buttons or multiple lines of text.

kotlin
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.*
import androidx.compose.runtime.Composable

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PlainTooltipExample() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
        tooltip = {
            PlainTooltip {
                Text("Add to favorites")
            }
        },
        state = rememberTooltipState()
    ) {
        IconButton(onClick = { }) {
            Icon(Icons.Filled.Favorite, contentDescription = "Favorite")
        }
    }
}

Running this snippet gives you a heart icon that, once pressed and held, shows a compact plain tooltip reading "Add to favorites" just above or below the icon depending on available space. This is the pattern most apps reach for first because a Jetpack Compose plain tooltip covers the vast majority of Jetpack Compose tooltip needs with almost no setup.

Creating a Rich Tooltip in Jetpack Compose

Sometimes a single line of text is not enough, and that is where the RichTooltip composable comes in. A Jetpack Compose rich tooltip supports an optional title, a body of descriptive text, and an optional action button, which makes it a better fit for onboarding hints or a Jetpack Compose tooltip that needs to explain something in more depth.

Unlike a Jetpack Compose plain tooltip, a Jetpack Compose rich tooltip is built from three separate slots: title, text, and action. Only the text slot is required, so you can mix and match depending on how much detail the rich tooltip needs to communicate.

kotlin
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RichTooltipExample() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
        tooltip = {
            RichTooltip(
                title = { Text("Settings") },
                action = {
                    TextButton(onClick = { }) {
                        Text("Learn more")
                    }
                }
            ) {
                Text("Open the settings screen to change notifications, theme, and account details.")
            }
        },
        state = rememberTooltipState(isPersistent = true)
    ) {
        IconButton(onClick = { }) {
            Icon(Icons.Filled.Settings, contentDescription = "Settings")
        }
    }
}

When a user long-presses the settings icon here, the Jetpack Compose rich tooltip appears with a bold title, a paragraph of explanatory text, and a tappable action button. Setting isPersistent to true, which is covered in more detail in the next section, keeps a Jetpack Compose rich tooltip visible until the user dismisses it instead of auto-hiding, which suits the extra content a rich tooltip usually carries.

Controlling Jetpack Compose Tooltip Visibility with Tooltip State

Every Jetpack Compose tooltip box needs a TooltipState to know when the tooltip should be shown or hidden. You create one with the rememberTooltipState function, and that state object exposes an isVisible property along with show and dismiss functions you can call from a coroutine.

The rememberTooltipState function accepts an isPersistent flag and a mutatorMutex. When isPersistent is false, the default, a Jetpack Compose tooltip automatically hides itself after a short delay or when the user taps elsewhere. When isPersistent is true, the tooltip stays open until you explicitly dismiss it, which is common for a Jetpack Compose rich tooltip that contains an action button.

Here is how to show a tooltip in Jetpack Compose programmatically, without waiting for a long press:

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManualTooltipStateExample() {
    val tooltipState = rememberTooltipState()
    val scope = rememberCoroutineScope()

    Column {
        TooltipBox(
            positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
            tooltip = { PlainTooltip { Text("Triggered from a button") } },
            state = tooltipState
        ) {
            Text("Hover or long-press this text")
        }

        Button(onClick = { scope.launch { tooltipState.show() } }) {
            Text("Show tooltip")
        }
    }
}

Tapping the button in this example launches a coroutine that calls show on the Jetpack Compose tooltip state, which flips the tooltip box into a visible tooltip without the user ever touching the anchor content directly. Calling dismiss on the same state object hides it again, giving you full manual control over a Jetpack Compose tooltip whenever a long press is not the right trigger.

Positioning a Jetpack Compose Tooltip

Every Jetpack Compose tooltip box needs a positionProvider to calculate where the tooltip should appear relative to its anchor. Jetpack Compose ships default providers through the TooltipDefaults object so you rarely need to write your own positioning math for a tooltip in Jetpack Compose.

Position providerUsed withWhat it does
rememberPlainTooltipPositionProviderPlainTooltipCenters the Jetpack Compose tooltip above the anchor with a small vertical offset
rememberRichTooltipPositionProviderRichTooltipAligns the Jetpack Compose tooltip below the anchor, accounting for the larger content size
Custom PositionProviderEither tooltip typeLets you calculate a custom offset when the default placement doesn't fit your layout

Choosing the matching position provider for the Jetpack Compose tooltip content you are using keeps the caret and popup aligned correctly. Mixing a plain tooltip position provider with rich tooltip content, for example, can leave the caret pointing at the wrong spot, so the pairing shown above is worth keeping consistent for every tooltip you add in Jetpack Compose.

Other Jetpack Compose Tooltip Box Parameters You Should Know

Beyond the tooltip content, state, and position provider, the Jetpack Compose TooltipBox composable exposes a handful of smaller parameters that fine-tune how a tooltip behaves in Jetpack Compose:

  • focusable: when true, the Jetpack Compose tooltip content can receive accessibility focus, which screen readers rely on to announce the tooltip text
  • enableUserInput: controls whether long-press and hover gestures on the anchor can trigger the tooltip; set it to false if you only want to show the Jetpack Compose tooltip programmatically
  • modifier: the standard Jetpack Compose modifier applied to the tooltip box itself, useful for padding or size constraints around the anchor
  • containerColor and contentColor: available on both PlainTooltip and RichTooltip to match the Jetpack Compose tooltip's surface and text to your app's color scheme
  • shape: lets you round or customize the Jetpack Compose tooltip's corner shape instead of using the Material 3 default
kotlin
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.RectangleShape

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipParametersExample() {
    TooltipBox(
        positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
        tooltip = {
            PlainTooltip(
                shape = RectangleShape,
                containerColor = MaterialTheme.colorScheme.primary,
                contentColor = MaterialTheme.colorScheme.onPrimary
            ) {
                Text("Share this item")
            }
        },
        state = rememberTooltipState(),
        focusable = true,
        enableUserInput = true
    ) {
        IconButton(onClick = { }) {
            Icon(Icons.Filled.Share, contentDescription = "Share")
        }
    }
}

This snippet combines several of the smaller parameters at once: a rectangular shape instead of the rounded default, custom container and content colors pulled from the app's theme, and both focusable and enableUserInput explicitly set to true so the tooltip stays both accessible and interactive in Jetpack Compose.

Practical Use Case: Jetpack Compose Tooltip Example for an Icon Button

A very common Jetpack Compose tooltip example is labeling a row of icon buttons in a toolbar where there isn't room for visible text labels. Without a Jetpack Compose tooltip, users often have to guess what an icon does; with one, the Jetpack Compose tooltip box turns any anchor composable into a self-explanatory control the moment it is pressed and held.

Picture a media player screen with icon buttons for shuffle, repeat, and download, none of which have visible text. Wrapping each icon in its own Jetpack Compose tooltip box lets every button describe itself on demand, without adding a line of text under every icon and crowding the toolbar.

kotlin
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MediaToolbarTooltipExample() {
    Row(modifier = Modifier.padding(12.dp)) {
        TooltipBox(
            positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
            tooltip = { PlainTooltip { Text("Shuffle playback") } },
            state = rememberTooltipState()
        ) {
            IconButton(onClick = { }) {
                Icon(Icons.Filled.Shuffle, contentDescription = "Shuffle")
            }
        }

        TooltipBox(
            positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
            tooltip = { PlainTooltip { Text("Repeat playback") } },
            state = rememberTooltipState()
        ) {
            IconButton(onClick = { }) {
                Icon(Icons.Filled.Repeat, contentDescription = "Repeat")
            }
        }

        TooltipBox(
            positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
            tooltip = { PlainTooltip { Text("Download for offline use") } },
            state = rememberTooltipState()
        ) {
            IconButton(onClick = { }) {
                Icon(Icons.Filled.Download, contentDescription = "Download")
            }
        }
    }
}

Each icon button here sits inside its own Jetpack Compose tooltip box, so long-pressing shuffle, repeat, or download shows a different plain tooltip for that specific anchor composable. This is the exact pattern to reach for any time a Jetpack Compose screen leans on icons alone and needs a lightweight way to label them without permanent on-screen text.

Complete Jetpack Compose Tooltip Example

The final example below ties together a Jetpack Compose tooltip box, a rich tooltip with a title and action, and a manually triggered state so you can see every piece of a tooltip working together in Jetpack Compose in one self-contained screen.

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CompleteTooltipScreen() {
    val tooltipState = rememberTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    Surface {
        Column(modifier = Modifier.padding(16.dp)) {
            TooltipBox(
                positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
                tooltip = {
                    RichTooltip(
                        title = { Text("Notifications") },
                        action = {
                            TextButton(onClick = { scope.launch { tooltipState.dismiss() } }) {
                                Text("Got it")
                            }
                        }
                    ) {
                        Text("Enable notifications to get alerts about new messages and updates.")
                    }
                },
                state = tooltipState
            ) {
                IconButton(onClick = { }) {
                    Icon(Icons.Filled.Notifications, contentDescription = "Notifications")
                }
            }

            Button(onClick = { scope.launch { tooltipState.show() } }) {
                Text("Show notifications tooltip")
            }
        }
    }
}

This screen wraps a notifications icon in a Jetpack Compose tooltip box using a rich tooltip, gives it a title, descriptive text, and a dismiss action, and adds a separate button that opens the same Jetpack Compose tooltip programmatically through the tooltip state. Running it produces a fully working tooltip in Jetpack Compose that a user can trigger either by long-pressing the icon or by tapping the button below it.