Tooltips in Material 3 Jetpack Compose (with Examples)

Jetpack Compose Tooltips

In this article, we’ll learn how to implement Tooltips in Material 3 Jetpack Compose.

Prerequisites:

What is a Tooltip?

Tooltips are used to display brief, informative text when a user hovers over or touches an element.

Example:

Tooltip Example

Note: Tooltips were introduced in Material 3 version 1.1.0. But, the ability to manage their state was added in 1.2.0-alpha02.

There are two types of tooltip APIs:

  1. PlainTooltipBox
  2. RichTooltipBox

1. PlainTooltipBox:

It just renders text on the screen.

Simple Plain Tooltip Example

2. RichTooltipBox:

It contains a heading, description, and button.

Rich Tooltip Example

Let’s see how to implement them in the Android Studio.

First, create an empty Compose project and open MainActivity. Create a MyUI() composable and call it from the onCreate() method. We’ll write our code in it.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltipBox
import androidx.compose.material3.RichTooltipBox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberPlainTooltipState
import androidx.compose.material3.rememberRichTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally   
                    ) {
                        MyUI()
                    }
                }
            }
        }
    }
}

@Composable
fun MyUI() {

}

1. PlainTooltipBox:

The API looks like this:

@Composable
@ExperimentalMaterial3Api
fun PlainTooltipBox(
    tooltip: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    focusable: Boolean = true,
    tooltipState: PlainTooltipState = rememberPlainTooltipState(),
    shape: Shape = TooltipDefaults.plainTooltipContainerShape,
    containerColor: Color = TooltipDefaults.plainTooltipContainerColor,   
    contentColor: Color = TooltipDefaults.plainTooltipContentColor,
    content: @Composable TooltipBoxScope.() -> Unit
)

tooltip – This is the tooltip’s content. We use Text() composable to add the text.

modifier – The Modifier to be applied to the tooltip.

focusable – Whether the tooltip is focusable or not.

tooltipState – It is to handle the state of the tooltip’s visibility.

shape – The Shape that should be applied to the tooltip container.

containerColor – The background color.

contentColor – Color that will be applied to the tooltip’s content.

content – The composable that the tooltip will anchor to. This is typically an icon button.

Plain Tooltip Example:

The tooltip and content are the mandatory parameters.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    PlainTooltipBox(
        tooltip = { Text("Add to favorites") }
    ) {
        IconButton(
            onClick = {
                // icon button click event
            },
            modifier = Modifier.tooltipTrigger()
        ) {
            Icon(
                imageVector = Icons.Filled.Favorite,   
                contentDescription = "Favorites",
                tint = Color(0xFF6650a4)
            )
        }
    }
}

Tap and hold the icon, you will see the following output:

Simple Plain Tooltip Example

Showing Plain Tooltip on a Button Click:

We can easily show a plain tooltip manually by managing its state. The state object can be obtained from the rememberPlainTooltipState() method.

val tooltipState = rememberPlainTooltipState()   

To display the tooltip, call the show() method. It is a suspend function. So, call it from the coroutine scope.

val tooltipState = rememberPlainTooltipState()
val scope = rememberCoroutineScope()

scope.launch { tooltipState.show() } // display the tooltip   

Example:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    val tooltipState = rememberPlainTooltipState()
    val scope = rememberCoroutineScope()

    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        PlainTooltipBox(
            tooltip = { Text("Add to list") },
            tooltipState = tooltipState
        ) {
            Icon(
                imageVector = Icons.Filled.AddCircle,
                contentDescription = "Add",
                tint = Color(0xFF6650a4)
            )
        }

        Spacer(Modifier.height(height = 30.dp))

        OutlinedButton(
            onClick = {
                scope.launch { tooltipState.show() } // show the tooltip   
            }
        ) {
            Text("Display tooltip")
        }
    }
}

Output:

Plain Tooltip on a Button Click

Related: Custom Button Designs Using Jetpack Compose

2. RichTooltipBox:

@Composable
@ExperimentalMaterial3Api
fun RichTooltipBox(
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    focusable: Boolean = false,
    title: (@Composable () -> Unit)? = null,
    action: (@Composable () -> Unit)? = null,
    tooltipState: RichTooltipState = rememberRichTooltipState(action != null),   
    shape: Shape = TooltipDefaults.richTooltipContainerShape,
    colors: RichTooltipColors = TooltipDefaults.richTooltipColors(),
    content: @Composable TooltipBoxScope.() -> Unit
)

It doesn’t have the tooltip parameter. Instead, it has text, title, and action parameters.

text – This is the message (description).

title – Title of the tooltip.

action – This is the action button. We should use TextButton().

Example:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    val tooltipState = rememberRichTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    RichTooltipBox(
        text = {
            // description
            Text(text = "Now, you can adjust the uploaded image quality and upgrade your storage space.")    
        },
        title = { Text(text = "New settings available") },
        action = {
            // learn more button
            TextButton(
                onClick = {
                    scope.launch { tooltipState.dismiss() } // dismiss the tooltip
                }
            ) {
                Text(text = "Learn more")
            }
        },
        tooltipState = tooltipState
    ) {
        // share button
        IconButton(
            onClick = {
                // share button click
            },
            modifier = Modifier.tooltipTrigger()
        ) {
            Icon(
                imageVector = Icons.Default.Share,
                contentDescription = "Share",
                tint = Color(0xFF6650a4)
            )
        }
    }
}

Output:

Rich Tooltip Example

The method rememberRichTooltipState() has isPersistent parameter. If it is true, then the tooltip will only be dismissed when the user clicks outside the bounds of the tooltip or if dismiss() is called. If it is false, the tooltip will dismiss after a short duration.

In the output, you can see that the text button has a left padding. This is likely a bug in the API. To fix this, add some left padding to the text and title.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    val tooltipState = rememberRichTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    RichTooltipBox(
        text = {
            // description
            Text(
                text = "Now, you can adjust the uploaded image quality and upgrade your storage space.",   
                modifier = Modifier.padding(start = 7.dp) // add padding 
            )
        },
        title = {
            Text(
                text = "New settings available",
                modifier = Modifier.padding(start = 7.dp) // add padding
            )
        },
        action = {
            // learn more button
            TextButton(
                onClick = {
                    scope.launch { tooltipState.dismiss() }
                }
            ) {
                Text(text = "Learn more")
            }
        },
        tooltipState = tooltipState
    ) {
        // share button
        IconButton(
            onClick = {
                // share button click
            },
            modifier = Modifier.tooltipTrigger()
        ) {
            Icon(
                imageVector = Icons.Default.Share,
                contentDescription = "Share",
                tint = Color(0xFF6650a4)
            )
        }
    }
}

Output:

Rich Tool Tip Problem Solution

Showing Rich Tooltip on a Button Click:

This is similar to the plain tooltip. Get the state object from the rememberRichTooltipState() method and call the show() method from the coroutine scope.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    val tooltipState = rememberRichTooltipState(isPersistent = true)
    val scope = rememberCoroutineScope()

    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        RichTooltipBox(
            title = {
                Text(
                    text = "New settings available",
                    modifier = Modifier.padding(start = 7.dp)
                )
            },
            action = {
                TextButton(
                    onClick = {
                        scope.launch {
                            tooltipState.dismiss() // dismiss the tooltip
                        }
                    }
                ) { Text(text = "Learn more") }
            },
            text = {
                Text(
                    text = "Now, you can adjust the uploaded image quality and upgrade your storage space.",   
                    modifier = Modifier.padding(start = 7.dp)
                )
            },
            tooltipState = tooltipState
        ) {
            Icon(
                imageVector = Icons.Filled.Info,
                contentDescription = null,
                tint = Color(0xFF6650a4)
            )
        }
        Spacer(Modifier.height(30.dp))
        OutlinedButton(
            onClick = {
                scope.launch {
                    tooltipState.show() // show the tooltip
                }
            }
        ) {
            Text("Display tooltip")
        }
    }
}

Output:

Jetpack Compose Rich Tooltip on a Button Click

This is all about Tooltips in Material 3 Jetpack Compose. I hope you have learned something new. If you have any doubts, comment below.

Related Articles:


References:

Leave a Comment