Build apps faster with our new App builder! Check it out →

ButtonGroup

Common

Component in Material 3 Compose

TODO link to mio page when available.

A layout composable that places its children in a horizontal sequence. When a child uses [Modifier.interactionSourceData] with a relevant [MutableInteractionSource], this button group can listen to the interactions and expand the width of the pressed child element as well as compress the neighboring child elements. Material3 components already use [Modifier.interactionSourceData] and will behave as expected.

TODO link to an image when available

@sample androidx.compose.material3.samples.ButtonGroupSample

A connected button group is a variant of a button group that have leading and trailing buttons that are asymmetric in shape and are used to make a selection.

Last updated:

Installation

dependencies {
   implementation("androidx.compose.material3:material3:1.4.0-alpha02")
}

Overloads

@Composable
@ExperimentalMaterial3ExpressiveApi
fun ButtonGroup(
    modifier: Modifier = Modifier,
    @FloatRange(0.0) animateFraction: Float = ButtonGroupDefaults.animateFraction,
    horizontalArrangement: Arrangement.Horizontal =
        Arrangement.spacedBy(ButtonGroupDefaults.spaceBetween),
    content: @Composable ButtonGroupScope.() -> Unit
)

Parameters

namedescription
modifierthe [Modifier] to be applied to the button group.
animateFractionthe percentage, represented by a float, of the width of the interacted child element that will be used to expand the interacted child element as well as compress the neighboring children.
horizontalArrangementThe horizontal arrangement of the button group's children.
contentthe content displayed in the button group, expected to use a Material3 component or a composable that is tagged with [Modifier.interactionSourceData].

Code Examples

ButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun ButtonGroupSample() {
    ButtonGroup {
        val options = listOf("A", "B", "C", "D")
        val checked = remember { mutableStateListOf(false, false, false, false) }
        val modifiers =
            listOf(
                Modifier.weight(1.5f),
                Modifier.weight(1f),
                Modifier.width(90.dp),
                Modifier.weight(1f)
            )
        options.fastForEachIndexed { index, label ->
            ToggleButton(
                checked = checked[index],
                onCheckedChange = { checked[index] = it },
                modifier = modifiers[index]
            ) {
                Text(label)
            }
        }
    }
}

MultiSelectConnectedButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MultiSelectConnectedButtonGroupSample() {
    val startButtonShapes =
        ButtonShapes(
            shape = ButtonGroupDefaults.connectedLeadingButtonShape,
            pressedShape = ButtonGroupDefaults.connectedLeadingButtonPressShape,
            checkedShape = ToggleButtonDefaults.checkedShape
        )
    val middleButtonShapes =
        ToggleButtonDefaults.shapes(
            ShapeDefaults.Small,
            ToggleButtonDefaults.pressedShape,
            ToggleButtonDefaults.checkedShape
        )
    val endButtonShapes =
        ButtonShapes(
            shape = ButtonGroupDefaults.connectedTrailingButtonShape,
            pressedShape = ButtonGroupDefaults.connectedTrailingButtonPressShape,
            checkedShape = ToggleButtonDefaults.checkedShape
        )
    val options = listOf("Work", "Restaurant", "Coffee")
    val unCheckedIcons =
        listOf(Icons.Outlined.Work, Icons.Outlined.Restaurant, Icons.Outlined.Coffee)
    val checkedIcons = listOf(Icons.Filled.Work, Icons.Filled.Restaurant, Icons.Filled.Coffee)
    val shapes = listOf(startButtonShapes, middleButtonShapes, endButtonShapes)
    val checked = remember { mutableStateListOf(false, false, false) }

    ButtonGroup(
        modifier = Modifier.padding(horizontal = 8.dp),
        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
        animateFraction = 0f
    ) {
        options.forEachIndexed { index, label ->
            ToggleButton(
                checked = checked[index],
                onCheckedChange = { checked[index] = it },
                shapes = shapes[index]
            ) {
                Icon(
                    if (checked[index]) checkedIcons[index] else unCheckedIcons[index],
                    contentDescription = "Localized description"
                )
                Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
                Text(label)
            }
        }
    }
}

SingleSelectConnectedButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SingleSelectConnectedButtonGroupSample() {
    val startButtonShapes =
        ButtonShapes(
            shape = ButtonGroupDefaults.connectedLeadingButtonShape,
            pressedShape = ButtonGroupDefaults.connectedLeadingButtonPressShape,
            checkedShape = ToggleButtonDefaults.checkedShape
        )
    val middleButtonShapes =
        ToggleButtonDefaults.shapes(
            ShapeDefaults.Small,
            ToggleButtonDefaults.pressedShape,
            ToggleButtonDefaults.checkedShape
        )
    val endButtonShapes =
        ButtonShapes(
            shape = ButtonGroupDefaults.connectedTrailingButtonShape,
            pressedShape = ButtonGroupDefaults.connectedTrailingButtonPressShape,
            checkedShape = ToggleButtonDefaults.checkedShape
        )
    val options = listOf("Work", "Restaurant", "Coffee")
    val unCheckedIcons =
        listOf(Icons.Outlined.Work, Icons.Outlined.Restaurant, Icons.Outlined.Coffee)
    val checkedIcons = listOf(Icons.Filled.Work, Icons.Filled.Restaurant, Icons.Filled.Coffee)
    val shapes = listOf(startButtonShapes, middleButtonShapes, endButtonShapes)
    var selectedIndex by remember { mutableIntStateOf(0) }

    ButtonGroup(
        modifier = Modifier.padding(horizontal = 8.dp),
        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.connectedSpaceBetween),
        animateFraction = 0f
    ) {
        options.forEachIndexed { index, label ->
            ToggleButton(
                checked = selectedIndex == index,
                onCheckedChange = { selectedIndex = index },
                shapes = shapes[index]
            ) {
                Icon(
                    if (selectedIndex == index) checkedIcons[index] else unCheckedIcons[index],
                    contentDescription = "Localized description"
                )
                Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
                Text(label)
            }
        }
    }
}
by @alexstyl