Pantrix UI

Tooltip

A short label that names the control it is anchored to: an icon button's name, a truncated label in full.

import com.pantrixui.components.overlay.tooltip.PantrixTooltip

Examples

Toolbar

At its plainest: PantrixTooltip wraps the control it names. Four buttons show only an icon: the tooltip shows each one's name and contentDescription says it to a screen reader. The two are the same words.

Rest the mouse on a button, focus it with the keyboard or long-press it with a finger: its name comes up. A click and a tap press the button; the long press that asks for the name does not. The card's "Last event" line says which button was pressed last.

val report = LocalExampleEvents.current
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixTooltip(text = "Refresh", tagKey = "refresh") {
        PantrixIconButton(
            imageVector = PantrixIcons.Refresh,
            contentDescription = "Refresh",
            onClick = { report("Refresh button pressed") },
            tagKey = "refresh"
        )
    }
    PantrixTooltip(text = "Search", tagKey = "search") {
        PantrixIconButton(
            imageVector = PantrixIcons.Search,
            contentDescription = "Search",
            onClick = { report("Search button pressed") },
            tagKey = "search"
        )
    }
    PantrixTooltip(text = "Zoom in", tagKey = "zoom_in") {
        PantrixIconButton(
            imageVector = PantrixIcons.ZoomIn,
            contentDescription = "Zoom in",
            onClick = { report("Zoom in button pressed") },
            tagKey = "zoom_in"
        )
    }
    PantrixTooltip(text = "Zoom out", tagKey = "zoom_out") {
        PantrixIconButton(
            imageVector = PantrixIcons.ZoomOut,
            contentDescription = "Zoom out",
            onClick = { report("Zoom out button pressed") },
            tagKey = "zoom_out"
        )
    }
}

Side

side says which side of the control the label sits on; the label is centred on the control on that side. By default it sits above the control: PopoverSide.Top. Start and End follow the reading direction: in a left-to-right layout Start is the left. Where the side asked for has no room, the label takes the opposite side.

var side by rememberSaveable { mutableStateOf(PopoverSide.Top) }
PantrixSegmentButton(options = SIDES, selected = side, onSelect = { side = it }, tagKey = "tooltip_side")
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
    PantrixTooltip(text = "side = ${side.name}", side = side, tagKey = "side") {
        PantrixButton(
            text = "Hover or long-press",
            onClick = {},
            variant = ButtonVariant.Outline,
            tagKey = "side_target"
        )
    }
}

private val SIDES = listOf(
    SegmentOption(value = PopoverSide.Top, label = "Top", tagKey = "top"),
    SegmentOption(value = PopoverSide.Bottom, label = "Bottom", tagKey = "bottom"),
    SegmentOption(value = PopoverSide.Start, label = "Start", tagKey = "start"),
    SegmentOption(value = PopoverSide.End, label = "End", tagKey = "end")
)

Truncated label

A file name cut short on one line because it does not fit its column: the tooltip shows the name in full. The tooltip's label is never wider than the style's maximum width (TooltipStyle.maxWidth); the long name wraps inside it.

The anchor here is not a control that is pressed but a text that is only read: the label comes up when the mouse rests on it or a finger long-presses it.

val fileName = "2026 third quarter sales report, northern region, second draft and notes.pdf"
PantrixTooltip(text = fileName, side = PopoverSide.Bottom, tagKey = "file") {
    PantrixText(text = fileName, modifier = Modifier.width(220.dp), maxLines = 1, tagKey = "file_name")
}

A hint the screen brings up

Whether the label is up is a state, and the screen can hold it itself: rememberPantrixTooltipState(). hint.show() brings the label up without anyone touching the control: a hint on the first run, the name of a control that has just changed. hint.dismiss() puts it away, and so does a press anywhere else.

The mouse, the keyboard and a long press change that same state too: hint.visible always tells the truth about whether the label is up.

val hint = rememberPantrixTooltipState()
val report = LocalExampleEvents.current
Row(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalAlignment = Alignment.CenterVertically
) {
    PantrixTooltip(
        text = "Add to favourites",
        side = PopoverSide.Bottom,
        state = hint,
        tagKey = "favourite"
    ) {
        PantrixIconButton(
            imageVector = PantrixIcons.Star,
            contentDescription = "Add to favourites",
            onClick = { report("Added to favourites") },
            tagKey = "favourite"
        )
    }
    PantrixButton(text = "Show hint", onClick = { hint.show() }, tagKey = "show_hint")
    PantrixButton(text = "Hide", onClick = { hint.dismiss() }, variant = ButtonVariant.Text, tagKey = "hide_hint")
}

Can be turned off

While enabled = false nothing brings the label up, and a label that is already up is put away. It is for a control whose name is already in view. Turn the switch on: the button's tooltip starts working.

var enabled by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixSwitchButton(
    checked = enabled,
    onCheckedChange = { enabled = it },
    label = "This button's tooltip is on",
    tagKey = "tooltip_on"
)
// The tooltip shows the button's name and says nothing new: a screen reader reads the button, not the label.
PantrixTooltip(text = "Save", enabled = enabled, tagKey = "save") {
    PantrixButton(text = "Save", onClick = { report("Save button pressed") }, tagKey = "save")
}

Its own style

style changes the geometry of this tooltip alone: the fields of TooltipStyle that are set change, and the rest comes from the theme's and the platform's style. The first button's label comes up the moment the mouse is over it: pointerRestMillis = 0. The second's label has fully rounded corners (ShapeRole.Full) and more room around its text, and sits 12dp from the button.

pointerRestMillis is the mouse's wait alone: keyboard focus brings the label up at once, and a finger's long press after the platform's long-press time. The style's wait is 400 milliseconds on Android and iOS, 500 on desktop and the web.

Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixTooltip(text = "Calendar", style = TooltipStyle(pointerRestMillis = 0), tagKey = "instant") {
        PantrixIconButton(
            imageVector = PantrixIcons.Calendar,
            contentDescription = "Calendar",
            onClick = {},
            tagKey = "instant"
        )
    }
    PantrixTooltip(
        text = "Inbox",
        style = TooltipStyle(
            shape = ShapeRole.Full,
            horizontalPadding = 16.dp,
            verticalPadding = 8.dp,
            gap = 12.dp
        ),
        tagKey = "roomy"
    ) {
        PantrixIconButton(
            imageVector = PantrixIcons.Inbox,
            contentDescription = "Inbox",
            onClick = {},
            tagKey = "roomy"
        )
    }
}

Its own colours

Given no colours, the label is in the tone opposite the theme's: dark in the light theme, light in the dark. colors gives the label colours of its own: here the app's own purple, which is in none of the theme's roles. Only the fields of TooltipColors that are set change.

On a fill that has no role, content is given too: the theme does not know what reads on that fill. Where the fill is the colour of one of the theme's roles, such as PantrixTheme.colors.primary.color, the text by itself takes the colour the theme pairs with that role.

val report = LocalExampleEvents.current
val own = ownColors()
PantrixTooltip(
    text = "Pro feature",
    colors = TooltipColors(container = own.color, content = own.onColor),
    tagKey = "own"
) {
    PantrixIconButton(
        imageVector = PantrixIcons.Info,
        contentDescription = "Pro feature",
        onClick = { report("Pro feature button pressed") },
        tagKey = "own"
    )
}

On a button that opens a popover

The tooltip of a button that opens a popover: PantrixTooltip is inside the popover's anchor. When the popover opens the button is under it, and the label is put away. It does not come back when the popover closes either: nobody is asking for the button's name then.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixPopover(
    visible = open,
    onDismissRequest = {
        open = false
        report("More popover closed")
    },
    anchor = {
        PantrixTooltip(text = "More", tagKey = "more") {
            PantrixIconButton(
                imageVector = PantrixIcons.MoreHorizontal,
                contentDescription = "More",
                onClick = { open = true },
                tagKey = "more"
            )
        }
    },
    side = PopoverSide.Bottom,
    tagKey = "more"
) {
    PantrixText(text = "Rename, move or delete.")
}

In a dialog

The tooltips of the buttons in a dialog: the label sits beside the control inside the dialog. The dialog takes the keyboard for itself, and a key pressed in it does not reach the page underneath. Escape still puts the open label away first; the next Escape closes the dialog.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixButton(
    text = "File details…",
    onClick = { open = true },
    variant = ButtonVariant.Outline,
    tagKey = "file_details"
)
PantrixDialog(
    visible = open,
    onDismissRequest = {
        open = false
        report("File details closed")
    },
    title = "Report.pdf",
    message = "2,4 MB. Modified yesterday.",
    actions = {
        PantrixButton(
            text = "Close",
            onClick = {
                open = false
                report("File details closed")
            },
            variant = ButtonVariant.Text,
            tagKey = "close_details"
        )
    },
    tagKey = "file_details"
) {
    Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
        PantrixTooltip(text = "Add to favourites", tagKey = "file_favourite") {
            PantrixIconButton(
                imageVector = PantrixIcons.Star,
                contentDescription = "Add to favourites",
                onClick = { report("Add to favourites button pressed") },
                tagKey = "file_favourite"
            )
        }
        PantrixTooltip(text = "Move to folder", tagKey = "file_move") {
            PantrixIconButton(
                imageVector = PantrixIcons.Folder,
                contentDescription = "Move to folder",
                onClick = { report("Move to folder button pressed") },
                tagKey = "file_move"
            )
        }
    }
}

Icon where it is narrow, text where it is wide

An icon alone where it is narrow, a button with text where it is wide. The button learns its own room from BoxWithPantrixWindow: where it is narrow a tooltip shows its name; where it is wide the name is written on the button and there is no tooltip.

This box composes its content while it measures, and when its room crosses a breakpoint it throws the old branch away inside the measuring pass. The label's layer is composed at the window's root, so nothing breaks even if the label is up at that moment; the label is also put away whenever the window changes size.

val report = LocalExampleEvents.current
BoxWithPantrixWindow(modifier = Modifier.fillMaxWidth()) { box ->
    if (box.isNarrow) {
        PantrixTooltip(text = "Files", side = PopoverSide.End, tagKey = "files") {
            PantrixIconButton(
                imageVector = PantrixIcons.Folder,
                contentDescription = "Files",
                onClick = { report("Files button pressed (icon)") },
                variant = ButtonVariant.Outline,
                tagKey = "files"
            )
        }
    } else {
        PantrixButton(
            text = "Files",
            onClick = { report("Files button pressed (text)") },
            variant = ButtonVariant.Outline,
            tagKey = "files"
        )
    }
}

Room that narrows while the window stays the same

Room that narrows while the window stays the same: as with a pane that is dragged or a sidebar that slides open. Press the button: three seconds later the box widens or narrows. Within those three seconds bring up the icon's label or open the popover: when the room changes, the branch that holds them is thrown away, and the label and the popover go with it.

The box is a layout that composes its content while it measures (BoxWithConstraints): when it crosses the breakpoint it throws away the branch inside it, in the middle of the measuring pass. The layers of an open label and an open popover are composed at the window's root, so once the branch is thrown away they close in the next frame.

var narrow by rememberSaveable { mutableStateOf(true) }
val report = LocalExampleEvents.current
val scope = rememberCoroutineScope()
PantrixButton(
    text = if (narrow) "Widen in 3 seconds" else "Narrow in 3 seconds",
    onClick = {
        scope.launch {
            delay(3_000L)
            narrow = !narrow
            report(if (narrow) "Room narrowed" else "Room widened")
        }
    },
    variant = ButtonVariant.Outline,
    tagKey = "room"
)
BoxWithConstraints(modifier = Modifier.width(if (narrow) 140.dp else 300.dp)) {
    if (maxWidth < 220.dp) IconsInTheRoom() else WrittenButtonsInTheRoom()
}

@Composable
private fun IconsInTheRoom() {
    var open by rememberSaveable { mutableStateOf(false) }
    val report = LocalExampleEvents.current
    Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
        PantrixTooltip(text = "Files", tagKey = "room_files") {
            PantrixIconButton(
                imageVector = PantrixIcons.Folder,
                contentDescription = "Files",
                onClick = {},
                variant = ButtonVariant.Outline,
                tagKey = "room_files"
            )
        }
        PantrixPopover(
            visible = open,
            onDismissRequest = {
                open = false
                report("Info popover closed")
            },
            anchor = {
                PantrixIconButton(
                    imageVector = PantrixIcons.Info,
                    contentDescription = "Info",
                    onClick = { open = true },
                    variant = ButtonVariant.Outline,
                    tagKey = "room_info"
                )
            },
            side = PopoverSide.Bottom,
            tagKey = "room_info"
        ) {
            PantrixText(text = "It is narrow here.")
        }
    }
}

@Composable
private fun WrittenButtonsInTheRoom() {
    var open by rememberSaveable { mutableStateOf(false) }
    val report = LocalExampleEvents.current
    Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
        PantrixButton(text = "Files", onClick = {}, variant = ButtonVariant.Outline, tagKey = "room_files")
        PantrixPopover(
            visible = open,
            onDismissRequest = {
                open = false
                report("Info popover closed")
            },
            anchor = {
                PantrixButton(
                    text = "Info",
                    onClick = { open = true },
                    variant = ButtonVariant.Outline,
                    tagKey = "room_info"
                )
            },
            side = PopoverSide.Bottom,
            tagKey = "room_info"
        ) {
            PantrixText(text = "It is wide here.")
        }
    }
}

API

text

A short label that names the control it is anchored to: an icon button's name, a truncated label in full.

ParameterTypeDefaultWhat it is
textStringwhat the control is called: a few words, and the ones the control itself says.
modifierModifierModifierapplied to the label, inside its layer. Not to the control.
sidePopoverSidePopoverSide.Topthe side of the control the tooltip prefers. PopoverSide.Start and PopoverSide.End follow the layout direction.
enabledBooleantruefalse puts it away and keeps it away, for a control whose name is already in view.
statePantrixTooltipStaterememberPantrixTooltipState()whether the tooltip is up, and the handle that shows and hides it.
styleTooltipStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsTooltipColors?nullthis call's own colours. Only the ones it sets change; the rest stay the theme's.
tagKeyString?nulla locale-independent key for automation: the label's tag is Tooltip_<tagKey>.
anchor@Composable () -> Unitthe control the tooltip names, laid out where this is called and as that place asks. A control that is pressed, a button or an icon, or a text that is only read: one with a long press of its own, a text field or text that can be selected, would have that press taken from it on a touch screen.

TooltipStyle

PantrixTooltip's geometry: how large the label is, how far it sits from what it names, how close it comes to the edges of the window, and how long a pointer rests on a control before its tooltip appears.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe label's corners.
horizontalPaddingDpDp.UnspecifiedBetween the text and the label's start and end edges.
verticalPaddingDpDp.UnspecifiedBetween the text and the label's top and bottom edges.
maxWidthDpDp.UnspecifiedThe widest the label gets: longer text wraps.
gapDpDp.UnspecifiedBetween what the tooltip names and the tooltip.
marginDpDp.UnspecifiedKept clear between the tooltip and the edges of the window, and of what covers the window there: a system bar, a cutout, the keyboard.
elevationDpDp.UnspecifiedHow high the label floats over the app, which is how large its shadow is.
pointerRestMillisInt?nullHow long a pointer rests on a control before its tooltip appears, in milliseconds. Keyboard focus shows it at once, and a finger's long press as long as the platform takes one to be: this is the pointer's wait alone. null leaves the value of the style underneath in place.

TooltipColors

What a PantrixTooltip is painted with, for a call that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe label's fill. The theme's onSurface: the tone opposite the theme's grounds, so that the label stands out over whatever the page shows under it, in the light theme and the dark.
contentColorColor.UnspecifiedThe label's text. The theme's textInverse, which is what reads on that tone.

Accessibility