Skip to content

#! /usr/bin/env markdown

Tooltip ​

Tooltip is a component in Miuix that briefly describes an anchor element. Following Material 3, a single TooltipBox anchors a tooltip to its content; you fill its tooltip slot with a PlainTooltip (a short inverse-surface label) or a RichTooltip (a surface-container card with an optional title and action). Tooltips are shown on hover (mouse) or long press (touch), or programmatically via TooltipState.

Import ​

kotlin
import top.yukonga.miuix.kmp.basic.TooltipBox
import top.yukonga.miuix.kmp.basic.RichTooltipBox
import top.yukonga.miuix.kmp.basic.PlainTooltip
import top.yukonga.miuix.kmp.basic.RichTooltip
import top.yukonga.miuix.kmp.basic.TooltipState
import top.yukonga.miuix.kmp.basic.rememberTooltipState
import top.yukonga.miuix.kmp.basic.TooltipDefaults
import top.yukonga.miuix.kmp.basic.TooltipAnchorPosition

Basic Usage ​

The quickest way to label an icon-only control is the text convenience overload of TooltipBox:

kotlin
TooltipBox(text = "Search") {
    IconButton(onClick = { /* ... */ }) {
        Icon(
            imageVector = MiuixIcons.Basic.Search,
            contentDescription = "Search",
        )
    }
}

TooltipBox ​

TooltipBox anchors a tooltip to its content. Its tooltip slot is a TooltipScope lambda, filled with PlainTooltip or RichTooltip.

kotlin
@Composable
fun TooltipBox(
    positionProvider: PopupPositionProvider,
    tooltip: @Composable TooltipScope.() -> Unit,
    state: TooltipState,
    modifier: Modifier = Modifier,
    focusable: Boolean = false,
    enableUserInput: Boolean = true,
    content: @Composable () -> Unit,
)
Parameter NameTypeDescriptionDefault ValueRequired
positionProviderPopupPositionProviderPositions the tooltip; see TooltipDefaults.rememberTooltipPositionProvider-Yes
tooltip@Composable TooltipScope.() -> UnitThe tooltip content (PlainTooltip / RichTooltip)-Yes
stateTooltipStateState controlling visibility-Yes
modifierModifierModifier applied to the anchor wrapperModifierNo
focusableBooleanWhether the tooltip is focusable (true for interactive rich tooltips)falseNo
enableUserInputBooleanWhether hover / long press on the anchor shows the tooltiptrueNo
content@Composable () -> UnitThe anchor content-Yes
kotlin
TooltipBox(
    positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
    tooltip = { PlainTooltip { Text("Add to favorites") } },
    state = rememberTooltipState(),
) {
    IconButton(onClick = { /* ... */ }) {
        Icon(imageVector = MiuixIcons.Basic.Check, contentDescription = "Add to favorites")
    }
}

PlainTooltip ​

PlainTooltip is a short, non-interactive label rendered on the Miuix inverse surface. It is a TooltipScope extension, so it is used inside the TooltipBox tooltip slot.

kotlin
@Composable
fun TooltipScope.PlainTooltip(
    modifier: Modifier = Modifier,
    caretShape: Shape? = null,
    maxWidth: Dp = TooltipDefaults.PlainTooltipMaxWidth,
    cornerRadius: Dp = TooltipDefaults.PlainTooltipCornerRadius,
    containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
    contentColor: Color = TooltipDefaults.plainTooltipContentColor,
    insideMargin: PaddingValues = TooltipDefaults.PlainTooltipInsideMargin,
    content: @Composable () -> Unit,
)

By default the plain tooltip uses an inverse surface tone (a dark bubble in light theme, a light bubble in dark theme). Pass caretShape = TooltipDefaults.caretShape() to draw a caret pointing at the anchor; the default is caret-less.

RichTooltip ​

RichTooltip is a persistent, interactive card with an optional title and action, rendered on a normal surfaceContainer. It is a TooltipScope extension.

kotlin
@Composable
fun TooltipScope.RichTooltip(
    modifier: Modifier = Modifier,
    title: (@Composable () -> Unit)? = null,
    action: (@Composable () -> Unit)? = null,
    caretShape: Shape? = null,
    maxWidth: Dp = TooltipDefaults.RichTooltipMaxWidth,
    cornerRadius: Dp = TooltipDefaults.RichTooltipCornerRadius,
    colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
    insideMargin: PaddingValues = TooltipDefaults.RichTooltipInsideMargin,
    text: @Composable () -> Unit,
)

A rich tooltip is interactive, so set focusable = true on the TooltipBox (the convenience RichTooltipBox does this for you) so an outside tap dismisses it and its action is reachable.

kotlin
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()

TooltipBox(
    positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
    tooltip = {
        RichTooltip(
            title = { Text("What's new") },
            action = {
                TextButton(text = "Got it", onClick = { tooltipState.dismiss() })
            },
        ) {
            Text("Rich tooltips support a title, supporting text, and an action.")
        }
    },
    state = tooltipState,
    focusable = true,
) {
    IconButton(onClick = { scope.launch { tooltipState.show() } }) {
        Icon(imageVector = MiuixIcons.Basic.Check, contentDescription = "What's new")
    }
}

TooltipState ​

TooltipState controls the tooltip's visibility. A global MutatorMutex keeps only one tooltip visible at a time.

kotlin
@Stable
interface TooltipState {
    val transition: MutableTransitionState<Boolean>
    val isVisible: Boolean
    val isPersistent: Boolean
    suspend fun show(mutatePriority: MutatePriority = MutatePriority.Default)
    fun dismiss()
    fun onDispose()
}

@Composable
fun rememberTooltipState(
    initialIsVisible: Boolean = false,
    isPersistent: Boolean = false,
    mutatorMutex: MutatorMutex = BasicTooltipDefaults.GlobalMutatorMutex,
): TooltipState
Parameter NameTypeDescriptionDefault ValueRequired
initialIsVisibleBooleanWhether the tooltip is initially visiblefalseNo
isPersistentBooleanWhether the tooltip stays visible until dismissedfalseNo
mutatorMutexMutatorMutexMutex ensuring only one tooltip shows at a timeBasicTooltipDefaults.GlobalMutatorMutexNo

When isPersistent is false (plain tooltips), the tooltip is dismissed automatically after a short timeout. When true (rich tooltips), it stays until dismiss(), an outside tap, or a back press. show() is a suspend function, so call it from a coroutine (e.g. scope.launch { state.show() }).

TooltipDefaults ​

TooltipDefaults provides position providers, colors, the caret, and dimension constants.

Position provider ​

kotlin
@Composable
fun TooltipDefaults.rememberTooltipPositionProvider(
    positioning: TooltipAnchorPosition = TooltipAnchorPosition.Below,
    spacingBetweenTooltipAndAnchor: Dp = TooltipDefaults.SpacingBetweenTooltipAndAnchor,
): PopupPositionProvider

TooltipAnchorPosition selects the preferred side: Above, Below, Left, Right, Start, End. The tooltip is centered on the cross axis and flips to the opposite side when there is not enough room.

Colors ​

kotlin
// Plain tooltip (inverse surface)
val plainContainer = TooltipDefaults.plainTooltipContainerColor   // onSecondaryVariant
val plainContent = TooltipDefaults.plainTooltipContentColor       // secondaryVariant

// Rich tooltip (surface container)
val richColors = TooltipDefaults.richTooltipColors(
    containerColor = MiuixTheme.colorScheme.surfaceContainer,
    contentColor = MiuixTheme.colorScheme.onSurfaceContainerVariant,
    titleContentColor = MiuixTheme.colorScheme.onSurfaceContainer,
    actionContentColor = MiuixTheme.colorScheme.primary,
)

RichTooltipColors is a data class of containerColor, contentColor, titleContentColor, and actionContentColor.

Caret ​

kotlin
fun TooltipDefaults.caretShape(): Shape
val TooltipDefaults.caretSize: DpSize // 16 x 8 dp

Pass caretShape = TooltipDefaults.caretShape() to PlainTooltip / RichTooltip to draw a caret pointing at the anchor (for Above / Below positioning). The default is caret-less, matching the rest of Miuix's floating surfaces.

Constants ​

Constant NameTypeDescriptionDefault Value
SpacingBetweenTooltipAndAnchorDpSpacing between a tooltip and its anchor8.dp
PlainTooltipMaxWidthDpMaximum width of a plain tooltip200.dp
PlainTooltipCornerRadiusDpCorner radius of a plain tooltip12.dp
PlainTooltipInsideMarginPaddingValuesInner padding of a plain tooltipPaddingValues(horizontal = 12.dp, vertical = 8.dp)
RichTooltipMaxWidthDpMaximum width of a rich tooltip320.dp
RichTooltipCornerRadiusDpCorner radius of a rich tooltip16.dp
RichTooltipInsideMarginPaddingValuesInner padding of a rich tooltipPaddingValues(all = 16.dp)
RichTooltipActionCornerRadiusDpCorner radius of a rich tooltip action button8.dp
RichTooltipActionInsideMarginPaddingValuesInner padding of a rich tooltip action buttonPaddingValues(horizontal = 12.dp, vertical = 6.dp)
caretSizeDpSizeSize of the caretDpSize(16.dp, 8.dp)

Convenience overloads ​

For the common cases you can skip the position provider and content composables.

Plain tooltip ​

kotlin
@Composable
fun TooltipBox(
    text: String,
    modifier: Modifier = Modifier,
    state: TooltipState = rememberTooltipState(isPersistent = false),
    enabled: Boolean = true,
    positioning: TooltipAnchorPosition = TooltipAnchorPosition.Below,
    containerColor: Color = TooltipDefaults.plainTooltipContainerColor,
    contentColor: Color = TooltipDefaults.plainTooltipContentColor,
    content: @Composable () -> Unit,
)

Rich tooltip ​

kotlin
@Composable
fun RichTooltipBox(
    text: String,
    modifier: Modifier = Modifier,
    state: TooltipState = rememberTooltipState(isPersistent = true),
    title: String? = null,
    actionText: String? = null,
    onActionClick: (() -> Unit)? = null,
    enabled: Boolean = true,
    positioning: TooltipAnchorPosition = TooltipAnchorPosition.Below,
    colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
    content: @Composable () -> Unit,
)

RichTooltipBox is persistent and focusable. Trigger it via long press / hover, or hoist state and call state.show() from the anchor's own click. The action invokes onActionClick then dismisses the tooltip.

kotlin
val richState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()

RichTooltipBox(
    title = "Rich tooltip",
    text = "Rich tooltips support a title, supporting text, and an action.",
    actionText = "Got it",
    onActionClick = { /* ... */ },
    state = richState,
) {
    TextButton(
        text = "Show rich tooltip",
        onClick = { scope.launch { richState.show() } },
    )
}

Changelog

Released under the Apache-2.0 License