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
PortalHostprovides the destination where tooltip panels are rendered.UnstyledTooltipmarks the interactive area the user can hover, focus, or long-press to show the tooltip.- The
anchorslot renders the content on which the tooltip will be anchored to. - The
panelslot renders the floating content that is shown for the anchor. TooltipPanelrenders 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. |