Compose Unstyled 2.0 is out! Check the official announcement blog ->

A tooltip component for contextual help on hover, focus, and long press.

import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.composables.icons.lucide.BellDot
import com.composables.icons.lucide.Lucide
import com.composeunstyled.AnchorAlignment
import com.composeunstyled.AnchorSide
import com.composeunstyled.PortalHost
import com.composeunstyled.TooltipPanel
import com.composeunstyled.TooltipPlacement
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.UnstyledTooltip
import com.composeunstyled.focusRing

@Composable
fun TooltipDemo() {
  PortalHost(Modifier.fillMaxSize()) {
    Box(
      modifier = Modifier.fillMaxSize(),
      contentAlignment = Alignment.Center,
    ) {
      UnstyledTooltip(
        side = AnchorSide.Top,
        alignment = AnchorAlignment.Center,
        panel = {
          TooltipPanel(
            enter = slideInVertically(tween(150), initialOffsetY = { (it * 0.25).toInt() }) +
              scaleIn(
                animationSpec = tween(150),
                transformOrigin = TransformOrigin(0.5f, 1f),
                initialScale = 0.65f,
              ) + fadeIn(tween(150)),
            exit = fadeOut(tween(250)),
          ) {
            TooltipBubble(it)
          }
        },
      ) {
        val interactionSource = remember { MutableInteractionSource() }

        UnstyledButton(
          onClick = { },
          modifier = Modifier
            .clip(CircleShape)
            .background(Color(0xFFF8FAFC))
            .border(1.dp, Color(0xFFCACACA), CircleShape)
            .focusRing(interactionSource, 1.dp, Color.Black, CircleShape),
          interactionSource = interactionSource,
        ) {
          Box(Modifier.padding(8.dp)) {
            UnstyledIcon(Lucide.BellDot, contentDescription = null)
          }
        }
      }
    }
  }
}

@Composable
private fun TooltipBubble(placement: TooltipPlacement) {
  when (placement.side) {
    AnchorSide.Top -> Column(horizontalAlignment = Alignment.CenterHorizontally) {
      TooltipContainer()
      TooltipArrow(placement)
    }

    AnchorSide.Bottom -> Column(horizontalAlignment = Alignment.CenterHorizontally) {
      TooltipArrow(placement)
      TooltipContainer()
    }

    AnchorSide.Start -> Row(verticalAlignment = Alignment.CenterVertically) {
      TooltipContainer()
      TooltipArrow(placement)
    }

    AnchorSide.End -> Row(verticalAlignment = Alignment.CenterVertically) {
      TooltipArrow(placement)
      TooltipContainer()
    }
  }
}

@Composable
private fun TooltipContainer() {
  Box(
    modifier = Modifier
      .clip(RoundedCornerShape(100))
      .background(Color(0xFFF8FAFC))
      .border(1.dp, Color(0xFFCACACA), RoundedCornerShape(100))
      .padding(vertical = 8.dp, horizontal = 12.dp),
  ) {
    BasicText("Notifications", style = TextStyle(color = Color.Black))
  }
}

@Composable
private fun TooltipArrow(placement: TooltipPlacement) {
  val arrowOffset = placement.positionAdjustment
  val modifier = when (placement.side) {
    AnchorSide.Top,
    AnchorSide.Bottom,
    -> Modifier.offset { IntOffset(-arrowOffset.x, 0) }

    AnchorSide.Start,
    AnchorSide.End,
    -> Modifier.offset { IntOffset(0, -arrowOffset.y) }
  }
  val degrees = when (placement.side) {
    AnchorSide.Top -> 180f
    AnchorSide.Bottom -> 0f
    AnchorSide.Start -> 270f
    AnchorSide.End -> 90f
  }

  ArrowUp(modifier.rotate(degrees), Color(0xFFCACACA))
}

@Composable
private fun ArrowUp(modifier: Modifier = Modifier, color: Color) {
  Canvas(modifier = modifier.size(8.dp, 4.dp)) {
    val path = Path().apply {
      moveTo(size.width / 2f, 0f)
      lineTo(0f, size.height)
      lineTo(size.width, size.height)
      close()
    }
    drawPath(path, color = color)
  }
}

Features

  • Focus, hover, and long-press triggers
  • Non-modal tooltip panel
  • Collision-aware positioning
  • Screen reader announcements

Installation

implementation("com.composables:composeunstyled-tooltip")

Anatomy

PortalHost {
  UnstyledTooltip(
    panel = {
      TooltipPanel {
      }
    },
  ) {
  }
}

Concepts

  • PortalHost provides the destination where tooltip panels are rendered.
  • UnstyledTooltip marks the interactive area the user can hover, focus, or long-press to show the tooltip.
  • The anchor slot renders the content on which the tooltip will be anchored to.
  • The panel slot renders the floating content that is shown for the anchor.
  • TooltipPanel renders the floating tooltip content.

Usage Considerations

UnstyledTooltip does not make the anchor focusable. Use focusable content in the anchor slot when the tooltip needs to show on keyboard focus.

Accessibility

TooltipPanel automatically announces the tooltip content when the tooltip becomes visible.

Code Examples

Positioning a tooltip

Use the side, alignment, sideOffset, and alignmentOffset parameters to place the tooltip relative to the anchor:

UnstyledTooltip(
  side = AnchorSide.Bottom,
  alignment = AnchorAlignment.Center,
  sideOffset = 8.dp,
  panel = {
    TooltipPanel {
      BasicText("More information")
    }
  },
) {
  BasicText("Help")
}

Delaying tooltip hover

Use the hoverDelayMillis parameter to wait before showing the tooltip on hover:

UnstyledTooltip(
  hoverDelayMillis = 500,
  panel = {
    TooltipPanel {
      BasicText("More information")
    }
  },
) {
  BasicText("Help")
}

Changing the long-press duration

Use the longPressShowDurationMillis parameter to change how long the tooltip stays visible after a long press:

UnstyledTooltip(
  longPressShowDurationMillis = 3_000,
  panel = {
    TooltipPanel {
      BasicText("More information")
    }
  },
) {
  BasicText("Help")
}

Animating a tooltip panel

Use the enter and exit parameters on TooltipPanel to animate the tooltip panel:

UnstyledTooltip(
  panel = {
    TooltipPanel(
      enter = fadeIn(),
      exit = fadeOut(),
    ) {
      BasicText("More information")
    }
  },
) {
  BasicText("Help")
}

API Reference

UnstyledTooltip

Parameter Type Description
enabled Boolean Whether the tooltip is enabled. When disabled, the tooltip will not show.
panel TooltipScope.() -> Unit A composable function that defines the tooltip content panel.
side AnchorSide
alignment AnchorAlignment
sideOffset Dp
alignmentOffset Dp
longPressShowDurationMillis Long Duration in milliseconds to show the tooltip after a long press. Default is 1500ms.
hoverDelayMillis Long Delay in milliseconds before showing the tooltip on hover. Default is 0ms.
anchor () -> Unit A composable function that defines the anchor element that triggers the tooltip.

TooltipPlacement

Parameter Type Description
side AnchorSide
alignment AnchorAlignment
positionAdjustment IntOffset

TooltipScope.TooltipPanel

Parameter Type Description
modifier Modifier Modifier to be applied to the tooltip panel.
enter EnterTransition The enter transition for the tooltip panel. Default is instant appearance.
exit ExitTransition The exit transition for the tooltip panel. Default is instant disappearance.
content (TooltipPlacement) -> Unit A composable function that defines the content of the tooltip.