Tooltips display contextual information when users interact with an element. They provide supplementary details without cluttering the interface and are triggered by hover, focus, or long-press actions.
Usage
import com.nomanr.lumo.ui.components.TooltipBox
import com.nomanr.lumo.ui.components.Tooltip
import com.nomanr.lumo.ui.components.rememberTooltipState
val tooltipState = rememberTooltipState()
TooltipBox(
tooltip = {
Tooltip {
Text("This is a tooltip")
}
},
state = tooltipState
) {
IconButton(onClick = { }) {
Icon(Icons.Default.Info, "Info")
}
}
Simple tooltip with text:
TooltipBox(
tooltip = {
Tooltip {
Text("Save your changes")
}
}
) {
IconButton(onClick = { /* Save */ }) {
Icon(Icons.Default.Save, "Save")
}
}
Default tooltip includes a caret (arrow) pointing to the anchor element:
val tooltipState = rememberTooltipState(isPersistent = true)
TooltipBox(
tooltip = {
Tooltip(
caretSize = DpSize(12.dp, 6.dp)
) {
Text("Tooltip with arrow")
}
},
state = tooltipState
) {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.Blue)
)
}
Remove the caret by setting caretSize to DpSize.Unspecified:
TooltipBox(
tooltip = {
Tooltip(
caretSize = DpSize.Unspecified
) {
Text("Simple tooltip without arrow")
}
}
) {
Text("Hover me")
}
Keep tooltip visible until explicitly dismissed:
val tooltipState = rememberTooltipState(
isPersistent = true
)
TooltipBox(
tooltip = {
Tooltip {
Text("This tooltip stays visible")
}
},
state = tooltipState
) {
Button(
onClick = {
scope.launch {
if (tooltipState.isVisible) {
tooltipState.dismiss()
} else {
tooltipState.show()
}
}
}
) {
Text("Toggle Tooltip")
}
}
Programmatic Control
Manually show and hide tooltips:
val tooltipState = rememberTooltipState()
val scope = rememberCoroutineScope()
TooltipBox(
tooltip = {
Tooltip { Text("Programmatically shown") }
},
state = tooltipState
) {
Button(
onClick = {
scope.launch {
tooltipState.show()
delay(2000)
tooltipState.dismiss()
}
}
) {
Text("Show Tooltip")
}
}
Parameters
tooltip
@Composable TooltipScope.() -> Unit
required
The tooltip content to display
modifier
Modifier
default:"Modifier"
Modifier for the tooltip box
state
TooltipState
default:"rememberTooltipState()"
State object to control the tooltip visibility
positionProvider
PopupPositionProvider
default:"rememberTooltipPositionProvider()"
Provides the position of the tooltip relative to the anchor
Whether the tooltip can receive focus
Whether user input is enabled while tooltip is shown
content
@Composable () -> Unit
required
The anchor content that triggers the tooltip
modifier
Modifier
default:"Modifier"
Modifier for the tooltip surface
caretSize
DpSize
default:"DpSize(12.dp, 6.dp)"
Size of the caret (arrow). Use DpSize.Unspecified to hide
Maximum width of the tooltip
shape
Shape
default:"RoundedCornerShape(4.dp)"
Shape of the tooltip container
containerColor
Color
default:"AppTheme.colors.surface"
Background color of the tooltip
Shadow elevation of the tooltip
content
@Composable () -> Unit
required
The content to display inside the tooltip
Initial visibility state of the tooltip
Whether the tooltip should persist until manually dismissed
mutatorMutex
MutatorMutex
default:"BasicTooltipDefaults.GlobalMutatorMutex"
Mutex to ensure only one tooltip is shown at a time
Custom Styling
Custom Colors
TooltipBox(
tooltip = {
Tooltip(
containerColor = Color(0xFF323232),
content = {
Text(
text = "Dark tooltip",
color = Color.White
)
}
)
}
) {
Icon(Icons.Default.Help, "Help")
}
Custom Shape and Size
TooltipBox(
tooltip = {
Tooltip(
shape = RoundedCornerShape(8.dp),
maxWidth = 200.dp,
caretSize = DpSize(16.dp, 8.dp)
) {
Column(modifier = Modifier.padding(8.dp)) {
Text("Custom Tooltip", style = AppTheme.typography.h4)
Text("With multiple lines", style = AppTheme.typography.body2)
}
}
}
) {
Button(onClick = {}) { Text("Hover") }
}
Rich Content
TooltipBox(
tooltip = {
Tooltip {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = AppTheme.colors.primary
)
Column {
Text("Feature Name", style = AppTheme.typography.h4)
Text("Description here", style = AppTheme.typography.body2)
}
}
}
}
) {
IconButton(onClick = {}) {
Icon(Icons.Default.Info, "Info")
}
}
Positioning
Custom position provider:
val customPositionProvider = remember {
object : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
// Custom positioning logic
return IntOffset(x = anchorBounds.right, y = anchorBounds.top)
}
}
}
TooltipBox(
tooltip = { Tooltip { Text("Custom position") } },
positionProvider = customPositionProvider
) {
Icon(Icons.Default.Star, "Star")
}
Defaults
- Caret Size: 12.dp × 6.dp
- Max Width: 300.dp
- Min Height: 24.dp
- Min Width: 40.dp
- Shadow Elevation: 4.dp
- Vertical Padding: 4.dp
- Horizontal Padding: 8.dp
- Shape: RoundedCornerShape(4.dp)
- Spacing Between Tooltip and Anchor: 4.dp
Animations
Tooltips include smooth fade and scale animations:
-
Scale Animation: 100ms with FastOutLinearInEasing
- Hidden: 0.8x scale
- Visible: 1.0x scale
-
Alpha Animation: 50ms with FastOutSlowInEasing
- Hidden: 0.0 alpha
- Visible: 1.0 alpha
Best Practices
- Keep it Brief: Tooltips should contain short, helpful text
- Don’t Repeat Labels: Don’t show the same text that’s already visible
- Accessibility: Essential information shouldn’t only be in tooltips
- Mobile Considerations: On touch devices, use long-press to show tooltips
- Consistent Styling: Maintain consistent tooltip styling across your app
- Positioning: Ensure tooltips don’t cover critical UI elements
- Timing: Use persistent tooltips sparingly, most should auto-dismiss
Accessibility
- Tooltips automatically include proper semantics
- Content descriptions are preserved from anchor elements
- Focus management is handled automatically
- Screen readers announce tooltip content appropriately
Source Reference
See the full implementation in Tooltip.kt:66-98 (TooltipBox) and Tooltip.kt:101-148 (Tooltip).