Use tooltips for short, contextual explanations that appear near an anchor.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Text
import com.composables.ui.components.Tooltip
import com.composables.ui.components.TooltipAlignment
import com.composables.ui.components.TooltipPanel
import com.composables.ui.components.TooltipSide
@Composable
fun TooltipExample() {
Tooltip(
panel = {
TooltipPanel {
Text("Tooltip")
}
},
) {
Button(onClick = {}, style = ButtonStyle.Outlined) {
Text("Hover me")
}
}
}Installation
implementation("com.composables:ui:0.1.0")Add the required dependencies
implementation("com.composables:composeunstyled:2.7.0")
Copy and paste the following sources into your project
components/Tooltip.kt
package com.composables.ui.components
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.LinearOutSlowInEasing
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.scaleOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.composables.ui.theme.borderColor
import com.composables.ui.theme.colors
import com.composables.ui.theme.onPanelColor
import com.composables.ui.theme.panelColor
import com.composeunstyled.AnchorAlignment
import com.composeunstyled.AnchorSide
import com.composeunstyled.LocalTextStyle
import com.composeunstyled.ProvideContentColor
import com.composeunstyled.ProvideTextStyle
import com.composeunstyled.UnstyledTooltip
import com.composeunstyled.theme.Theme
import kotlin.jvm.JvmInline
import com.composeunstyled.TooltipPanel as UnstyledTooltipPanel
import com.composeunstyled.TooltipScope as UnstyledTooltipScope
private const val TooltipEnterDurationMillis = 300
private const val TooltipExitDurationMillis = 250
class TooltipScope internal constructor(
internal val unstyledScope: UnstyledTooltipScope,
internal val side: TooltipSide,
internal val alignment: TooltipAlignment,
)
/**
* Side options for tooltip placement relative to its anchor.
*/
@JvmInline
value class TooltipSide internal constructor(private val value: Int) {
internal val anchorSide: AnchorSide
get() = when (this) {
Bottom -> AnchorSide.Bottom
Start -> AnchorSide.Start
End -> AnchorSide.End
else -> AnchorSide.Top
}
companion object {
/**
* Places the tooltip above its anchor.
*/
val Top = TooltipSide(0)
/**
* Places the tooltip below its anchor.
*/
val Bottom = TooltipSide(1)
/**
* Places the tooltip before its anchor in the layout direction.
*/
val Start = TooltipSide(2)
/**
* Places the tooltip after its anchor in the layout direction.
*/
val End = TooltipSide(3)
}
}
/**
* Alignment options for tooltip placement along the anchor edge.
*/
@JvmInline
value class TooltipAlignment internal constructor(private val value: Int) {
internal val anchorAlignment: AnchorAlignment
get() = when (this) {
Start -> AnchorAlignment.Start
End -> AnchorAlignment.End
else -> AnchorAlignment.Center
}
companion object {
/**
* Aligns the tooltip to the start edge of the anchor.
*/
val Start = TooltipAlignment(0)
/**
* Centers the tooltip against the anchor.
*/
val Center = TooltipAlignment(1)
/**
* Aligns the tooltip to the end edge of the anchor.
*/
val End = TooltipAlignment(2)
}
}
/**
* An anchored tooltip with configurable placement and timing.
* @param enabled Whether the tooltip can be shown.
* @param side Side of the anchor where the tooltip should appear.
* @param alignment Alignment of the tooltip along the chosen side.
* @param sideOffset Distance between the tooltip and its anchor.
* @param alignmentOffset Offset applied along the anchor edge.
* @param longPressShowDurationMillis Duration to keep the tooltip visible after a long press.
* @param hoverDelayMillis Delay before showing the tooltip on hover.
* @param panel Composable tooltip panel content.
* @param anchor Composable anchor that triggers the tooltip.
*/
@Composable
fun Tooltip(
enabled: Boolean = true,
side: TooltipSide = TooltipSide.Top,
alignment: TooltipAlignment = TooltipAlignment.Center,
sideOffset: Dp = 8.dp,
alignmentOffset: Dp = 0.dp,
longPressShowDurationMillis: Long = 1500L,
hoverDelayMillis: Long = 0L,
panel: @Composable TooltipScope.() -> Unit,
anchor: @Composable () -> Unit,
) {
UnstyledTooltip(
enabled = enabled,
panel = {
TooltipScope(
unstyledScope = this,
side = side,
alignment = alignment,
).panel()
},
side = side.anchorSide,
alignment = alignment.anchorAlignment,
sideOffset = sideOffset,
alignmentOffset = alignmentOffset,
longPressShowDurationMillis = longPressShowDurationMillis,
hoverDelayMillis = hoverDelayMillis,
anchor = anchor,
)
}
/**
* A styled tooltip surface drawn near the anchor.
* @param modifier Modifier applied to the component.
* @param shape Shape used for the tooltip panel.
* @param backgroundColor Background color used for the tooltip panel.
* @param contentColor Color used for tooltip content.
* @param contentPadding Padding applied inside the tooltip panel.
* @param enter Transition used when the tooltip appears.
* @param exit Transition used when the tooltip disappears.
* @param content Composable content displayed by the component.
*/
@Composable
fun TooltipScope.TooltipPanel(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(6.dp),
backgroundColor: Color = Theme[colors][panelColor],
contentColor: Color = Theme[colors][onPanelColor],
outlineColor: Color = Theme[colors][borderColor],
contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
enter: EnterTransition? = null,
exit: ExitTransition? = null,
content: @Composable () -> Unit,
) {
val transformOrigin = tooltipTransformOrigin(side, alignment)
with(unstyledScope) {
UnstyledTooltipPanel(
modifier = modifier.zIndex(15f),
enter = enter ?: tooltipEnterTransition(transformOrigin),
exit = exit ?: tooltipExitTransition(transformOrigin),
) {
Box(
modifier = Modifier
.border(1.dp, outlineColor, shape)
.clip(shape)
.background(backgroundColor, shape)
.padding(contentPadding),
) {
ProvideContentColor(contentColor) {
ProvideTextStyle(LocalTextStyle.current.merge(TooltipTextStyle)) {
content()
}
}
}
}
}
}
private val TooltipTextStyle = TextStyle()
private fun tooltipEnterTransition(transformOrigin: TransformOrigin): EnterTransition {
return scaleIn(
animationSpec = tween(
durationMillis = TooltipEnterDurationMillis,
easing = LinearOutSlowInEasing,
),
initialScale = 0.96f,
transformOrigin = transformOrigin,
) + fadeIn(tween(durationMillis = TooltipEnterDurationMillis))
}
private fun tooltipExitTransition(transformOrigin: TransformOrigin): ExitTransition {
return scaleOut(
animationSpec = tween(durationMillis = TooltipExitDurationMillis),
targetScale = 0.96f,
transformOrigin = transformOrigin,
) + fadeOut(tween(durationMillis = TooltipExitDurationMillis))
}
private fun tooltipTransformOrigin(
side: TooltipSide,
alignment: TooltipAlignment,
): TransformOrigin {
return when (side) {
TooltipSide.Top -> TransformOrigin(
pivotFractionX = alignment.transformOriginFraction,
pivotFractionY = 1f,
)
TooltipSide.Bottom -> TransformOrigin(
pivotFractionX = alignment.transformOriginFraction,
pivotFractionY = 0f,
)
TooltipSide.Start -> TransformOrigin(
pivotFractionX = 1f,
pivotFractionY = alignment.transformOriginFraction,
)
else -> TransformOrigin(
pivotFractionX = 0f,
pivotFractionY = alignment.transformOriginFraction,
)
}
}
private val TooltipAlignment.transformOriginFraction: Float
get() = when (this) {
TooltipAlignment.Start -> 0f
TooltipAlignment.Center -> 0.5f
else -> 1f
}components/Utils.kt
package com.composables.ui.components
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.composables.ui.theme.colors
import com.composables.ui.theme.ringColor
import com.composeunstyled.FocusRingVisibility
import com.composeunstyled.collectIsFocusVisibleAsState
import com.composeunstyled.outline
import com.composeunstyled.theme.Theme
@Composable
fun Modifier.focusRing(
interactionSource: InteractionSource,
width: Dp = 2.dp,
color: Color = Theme[colors][ringColor],
shape: Shape = RectangleShape,
offset: Dp = 0.dp,
visibility: FocusRingVisibility = FocusRingVisibility.FocusVisible,
): Modifier {
val showFocusRing by if (visibility == FocusRingVisibility.FocusVisible) {
interactionSource.collectIsFocusVisibleAsState()
} else {
interactionSource.collectIsFocusedAsState()
}
val animatedWidth by animateDpAsState(
targetValue = if (showFocusRing) width else 0.dp,
animationSpec = tween(durationMillis = 120),
label = "FocusRingWidth",
)
return this then Modifier.outline(
width = animatedWidth,
color = color,
shape = shape,
offset = offset,
)
}
@Composable
fun Modifier.bouncyPress(
interactionSource: InteractionSource,
enabled: Boolean = true,
pressedScale: Float = 0.98f,
): Modifier {
val pressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (enabled && pressed) pressedScale else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow,
),
label = "BouncyPressScale",
)
return this then Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
}
}Examples
Side
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Text
import com.composables.ui.components.Tooltip
import com.composables.ui.components.TooltipPanel
import com.composables.ui.components.TooltipSide
@Composable
fun TooltipSideExample() {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
TooltipButton(
side = TooltipSide.Start,
label = "Start",
tooltipText = "Placed on the leading side",
)
TooltipButton(
side = TooltipSide.Top,
label = "Top",
tooltipText = "Placed above the anchor",
)
TooltipButton(
side = TooltipSide.Bottom,
label = "Bottom",
tooltipText = "Placed below the anchor",
)
TooltipButton(
side = TooltipSide.End,
label = "End",
tooltipText = "Placed on the trailing side",
)
}
}
@Composable
private fun TooltipButton(
label: String,
tooltipText: String,
side: TooltipSide,
) {
Tooltip(
side = side,
panel = { TooltipPanel { Text(text = tooltipText) } },
) {
Button(onClick = {}, style = ButtonStyle.Outlined) {
Text(text = label)
}
}
}Alignment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Text
import com.composables.ui.components.Tooltip
import com.composables.ui.components.TooltipAlignment
import com.composables.ui.components.TooltipPanel
@Composable
fun TooltipAlignmentExample() {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
TooltipButton(
label = "Start",
tooltipText = "Aligned to the start edge",
alignment = TooltipAlignment.Start,
)
TooltipButton(
label = "Center",
tooltipText = "Aligned to the center",
alignment = TooltipAlignment.Center,
)
TooltipButton(
label = "End",
tooltipText = "Aligned to the end edge",
alignment = TooltipAlignment.End,
)
}
}
@Composable
private fun TooltipButton(
label: String,
tooltipText: String,
alignment: TooltipAlignment,
) {
Tooltip(
alignment = alignment,
panel = { TooltipPanel { Text(text = tooltipText) } },
) {
Button(onClick = {}, style = ButtonStyle.Outlined) {
Text(text = label)
}
}
}Hover delay
import androidx.compose.runtime.Composable
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Text
import com.composables.ui.components.Tooltip
import com.composables.ui.components.TooltipPanel
@Composable
fun TooltipHoverDelayExample() {
Tooltip(
hoverDelayMillis = 600L,
panel = {
TooltipPanel {
Text("Tooltip")
}
},
) {
Button(onClick = {}, style = ButtonStyle.Outlined) {
Text("Hover me")
}
}
}Long press duration
import androidx.compose.runtime.Composable
import com.composables.ui.components.Button
import com.composables.ui.components.ButtonStyle
import com.composables.ui.components.Text
import com.composables.ui.components.Tooltip
import com.composables.ui.components.TooltipPanel
@Composable
fun TooltipLongPressDurationExample() {
Tooltip(
longPressShowDurationMillis = 3_000L,
panel = {
TooltipPanel {
Text("Tooltip")
}
},
) {
Button(onClick = {}, style = ButtonStyle.Outlined) {
Text("Long press me")
}
}
}API Reference
Tooltip
An anchored tooltip with configurable placement and timing.
@Composable
fun Tooltip(
enabled: Boolean = true,
side: TooltipSide = TooltipSide.Top,
alignment: TooltipAlignment = TooltipAlignment.Center,
sideOffset: Dp = 8.dp,
alignmentOffset: Dp = 0.dp,
longPressShowDurationMillis: Long = 1500L,
hoverDelayMillis: Long = 0L,
panel: @Composable TooltipScope.() -> Unit,
anchor: @Composable () -> Unit,
)
| Parameter | Type | Description |
|---|---|---|
enabled |
Boolean |
Whether the tooltip can be shown. |
side |
TooltipSide |
Side of the anchor where the tooltip should appear. |
alignment |
TooltipAlignment |
Alignment of the tooltip along the chosen side. |
sideOffset |
Dp |
Distance between the tooltip and its anchor. |
alignmentOffset |
Dp |
Offset applied along the anchor edge. |
longPressShowDurationMillis |
Long |
Duration to keep the tooltip visible after a long press. |
hoverDelayMillis |
Long |
Delay before showing the tooltip on hover. |
panel |
@Composable TooltipScope.() -> Unit |
Composable tooltip panel content. |
anchor |
@Composable () -> Unit |
Composable anchor that triggers the tooltip. |
TooltipPanel
A styled tooltip surface drawn near the anchor.
@Composable
fun TooltipScope.TooltipPanel(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(6.dp),
backgroundColor: Color = Theme[colors][panelColor],
contentColor: Color = Theme[colors][onPanelColor],
outlineColor: Color = Theme[colors][borderColor],
contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp, vertical = 6.dp),
enter: EnterTransition? = null,
exit: ExitTransition? = null,
content: @Composable () -> Unit,
)
| Parameter | Type | Description |
|---|---|---|
modifier |
Modifier |
Modifier applied to the component. |
shape |
Shape |
Shape used for the tooltip panel. |
backgroundColor |
Color |
Background color used for the tooltip panel. |
contentColor |
Color |
Color used for tooltip content. |
outlineColor |
Color |
|
contentPadding |
PaddingValues |
Padding applied inside the tooltip panel. |
enter |
EnterTransition? |
Transition used when the tooltip appears. |
exit |
ExitTransition? |
Transition used when the tooltip disappears. |
content |
@Composable () -> Unit |
Composable content displayed by the component. |
TooltipSide
Side options for tooltip placement relative to its anchor.
@JvmInline
value class TooltipSide internal constructor(private val value: Int)
| Value | Description |
|---|---|
Top |
Places the tooltip above its anchor. |
Bottom |
Places the tooltip below its anchor. |
Start |
Places the tooltip before its anchor in the layout direction. |
End |
Places the tooltip after its anchor in the layout direction. |
TooltipAlignment
Alignment options for tooltip placement along the anchor edge.
@JvmInline
value class TooltipAlignment internal constructor(private val value: Int)
| Value | Description |
|---|---|
Start |
Aligns the tooltip to the start edge of the anchor. |
Center |
Centers the tooltip against the anchor. |
End |
Aligns the tooltip to the end edge of the anchor. |