Compose Unstyled v1.47: Tooltips, Stack and responsive goodies

Alex Styl@alexstyl

It's been a moment since the last Compose Unstyled version update. There have been quite a few releases since then, but I never sent out any updates for it.

To be really honest, I just wanted to just show off the new interactive demos we can now use on the website so expect more immersive blog posts and documentation on Composables.

Here is what changed since the last update:

New Tooltip Component added

The new Tooltip primitive component allows you to create informational tooltips for your apps. It is automatically displayed when you long-press its trigger, mouse over it or when the trigger is focused.

The Unstyled Tooltip is a complete rewrite of Tooltips in Compose and does not use the standard Compose Tooltip API. This is because the Compose Tooltip API does not work with keyboards and mouse as it is modal.

The Unstyled Tooltip is not modal and therefore does not steal focus or block pointer events.

@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

@Composable
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)
    }
}

@Composable
fun TooltipExample() {
    Tooltip(
        placement = RelativeAlignment.TopCenter,
        panel = {
            TooltipPanel(
                modifier = Modifier.zIndex(15f),
                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)),
                arrow = { direction ->
                    val degrees = when (direction) {
                        TooltipArrowDirection.Up -> 0f
                        TooltipArrowDirection.Down -> 180f
                        TooltipArrowDirection.Left -> 90f
                        TooltipArrowDirection.Right -> 270f
                    }
                    ArrowUp(Modifier.rotate(degrees), Color.Black.copy(0.8f))
                }
            ) {
                Box(
                    modifier = Modifier
                        .clip(RoundedCornerShape(100))
                        .background(Color.Black.copy(0.8f))
                        .padding(vertical = 8.dp, horizontal = 12.dp),
                ) {
                    Text("This is a tooltip", color = Color.White)
                }
            }
        }
    ) {
        Button(
            onClick = { },
            contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
            backgroundColor = Color.White,
            shape = RoundedCornerShape(8.dp),
            modifier = Modifier.border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(8.dp))
        ) {
            Text("Hover me")
        }
    }
}

Checkout the Tooltip documentation for more details.

Custom Detent support for sheets added

You can now dynamically control the detents of your sheets by using the new detents setter on the state class.

If the detent in which the sheet is rested at does not exist anymore, then it will be animated to the closest detent available.

For more details, checkout the Bottom Sheet documentation.

New Stack component added

The new Stack component is a flexible layout composable that can arrange its children either horizontally or vertically. You can control using the orientation parameter.

This is incredibly handy when you are building responsive apps. You can use this with the new [currentWindowContainerSize()] function to get the size of the container and change the orientation accordingly.

@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

import com.composeunstyled.Stack
import com.composeunstyled.StackOrientation
import com.composeunstyled.currentWindowContainerSize

var orientation by remember { mutableStateOf(StackOrientation.Horizontal) }

Stack(
    orientation = orientation,
    spacing = 16.dp
) {
    Box(/*...*/) {
        Text("0")
    }
    Box(/*...*/) {
        Text("1")
    }
    Box(/*...*/) {
        Text("2")
    }
}

For more details, checkout the Stack documentation.

New currentWindowContainerSize() fun added

A new utility function that returns the size of the window in which your Composable is rendered on.

@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

HINT: Resize your browser's width to see the size changing.

import com.composeunstyled.currentWindowContainerSize

val containerSize = currentWindowContainerSize()

Stateful TextField added

Compose has recently introduced new versions of BasicTextField which are stateful. It is recommended to use those over the original stateless overload, as they fix a bug of issues with soft-keyboard glitches in various languages.

TextFields in Compose Unstyled now support the same state objects using the rememberTextFieldState() function.

For more details, head over the TextField docs.


That's all for this update. Catch you in the next one.

– Alex


Discussion