Connected button group

Toggle buttons that share one row and morph their shape on press and selection.

When to use

Use a connected group for a small set of related options where one or more can be active. The outer ends are fully round, the inner corners are small, and the selected button turns fully round.

Build it from a Row of ToggleButtons. Pick the leading, middle or trailing shapes from ButtonGroupDefaults by position, and use ConnectedSpaceBetween for the gap.

Examples

Single selection

Exactly one option is active at a time.

val options = listOf("Day", "Week", "Month")
var selected by rememberSaveable { mutableIntStateOf(0) }

Row(horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween)) {
    options.forEachIndexed { index, label ->
        ToggleButton(
            checked = selected == index,
            onCheckedChange = { selected = index },
            shapes = when (index) {
                0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
            },
        ) {
            Text(label)
        }
    }
}

Multiple selection

Any number of options can be active.

val options = listOf("Bold", "Italic", "Underline")
val selected = remember { mutableStateOf(setOf(0)) }

Row(horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween)) {
    options.forEachIndexed { index, label ->
        ToggleButton(
            checked = index in selected.value,
            onCheckedChange = { checked ->
                selected.value = if (checked) selected.value + index else selected.value - index
            },
            shapes = when (index) {
                0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
            },
        ) {
            Text(label)
        }
    }
}

Icons only

Icon labels need a content description.

val options = listOf(Icons.Rounded.ViewDay to "Day view", Icons.Rounded.ViewWeek to "Week view")
var selected by rememberSaveable { mutableIntStateOf(0) }

Row(horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween)) {
    options.forEachIndexed { index, (icon, description) ->
        ToggleButton(
            checked = selected == index,
            onCheckedChange = { selected = index },
            shapes = if (index == 0) {
                ButtonGroupDefaults.connectedLeadingButtonShapes()
            } else {
                ButtonGroupDefaults.connectedTrailingButtonShapes()
            },
        ) {
            Icon(icon, contentDescription = description, modifier = Modifier.size(20.dp))
        }
    }
}

Guidelines

Do:

Don't:

Accessibility

Related components