Connected button group
Toggle buttons that share one row and morph their shape on press and selection.
- Category: Actions
- API status: Expressive
- Compose APIs:
ToggleButton,ButtonGroupDefaults.connectedLeadingButtonShapes,ButtonGroupDefaults.ConnectedSpaceBetween - Interactive page: https://m3e.ljzapps.app/#components/connected-button-group
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:
- Keep labels short and parallel.
- Use three to five options.
Don't:
- Do not use a connected group for navigation.
- Do not wrap options onto a second row.
Accessibility
- Each ToggleButton exposes its checked state. Group related buttons with a visible label nearby.
- Icon-only options must have a contentDescription.