Compose Unstyled 2.0 is out! Check the official announcement blog ->
Compose Component

ButtonGroup

A layout composable that places its children in a horizontal sequence.

ButtonGroup social preview

ButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun ButtonGroupSample() {
    val numButtons = 10
    ButtonGroup(
        overflowIndicator = { menuState ->
            ButtonGroupDefaults.OverflowIndicator(menuState = menuState)
        },
        verticalAlignment = Alignment.Top,
    ) {
        for (i in 0 until numButtons) {
            clickableItem(onClick = {}, label = "$i")
        }
    }
}

ButtonGroupWithCustomItemSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun ButtonGroupWithCustomItemSample() {
    val options = listOf("Work", "Restaurant", "Home")
    val unCheckedIcons = listOf(Icons.Outlined.Work, Icons.Outlined.Restaurant, Icons.Outlined.Home)
    val checkedIcons = listOf(Icons.Filled.Work, Icons.Filled.Restaurant, Icons.Filled.Home)
    val checked = remember { mutableStateListOf(false, false, false) }
    val interactionSources = remember { List(options.size) { MutableInteractionSource() } }
    ButtonGroup(
        overflowIndicator = { menuState ->
            ButtonGroupDefaults.OverflowIndicator(menuState = menuState)
        },
        expandedRatio = 1f,
    ) {
        options.forEachIndexed { index, label ->
            customItem(
                buttonGroupContent = {
                    ToggleButton(
                        checked = checked[index],
                        onCheckedChange = { checked[index] = it },
                        shapes =
                            when (index) {
                                0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                                options.lastIndex ->
                                    ButtonGroupDefaults.connectedTrailingButtonShapes()
                                else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
                            },
                        contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
                        interactionSource = interactionSources[index],
                        modifier =
                            Modifier.animateWidth(
                                interactionSource = interactionSources[index],
                                compressionLimit = ButtonDefaults.ButtonWithIconContentPadding,
                            ),
                    ) {
                        Icon(
                            if (checked[index]) checkedIcons[index] else unCheckedIcons[index],
                            contentDescription = "Localized description",
                        )
                        Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
                        Text(
                            text = label,
                            softWrap = false,
                            maxLines = 1,
                            overflow = TextOverflow.Visible,
                        )
                    }
                },
                menuContent = {
                    DropdownMenuItem(
                        leadingIcon = { checkedIcons[index] },
                        text = { Text(label) },
                        onClick = {},
                        interactionSource = interactionSources[index],
                    )
                },
            )
        }
    }
}

MultiSelectConnectedButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MultiSelectConnectedButtonGroupSample() {
    val options = listOf("Work", "Restaurant", "Coffee", "Search", "Home")
    val unCheckedIcons =
        listOf(
            Icons.Outlined.Work,
            Icons.Outlined.Restaurant,
            Icons.Outlined.Coffee,
            Icons.Outlined.Search,
            Icons.Outlined.Home,
        )
    val checkedIcons =
        listOf(
            Icons.Filled.Work,
            Icons.Filled.Restaurant,
            Icons.Filled.Coffee,
            Icons.Filled.Search,
            Icons.Filled.Home,
        )
    val checked = remember { mutableStateListOf(false, false, false, false, false) }
    FlowRow(
        Modifier.padding(horizontal = 8.dp).fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
        verticalArrangement = Arrangement.spacedBy(2.dp),
    ) {
        options.forEachIndexed { index, label ->
            ToggleButton(
                checked = checked[index],
                onCheckedChange = { checked[index] = it },
                shapes =
                    when (index) {
                        0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                        options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                        else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
                    },
            ) {
                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 options = listOf("Work", "Restaurant", "Coffee", "Search", "Home")
    val unCheckedIcons =
        listOf(
            Icons.Outlined.Work,
            Icons.Outlined.Restaurant,
            Icons.Outlined.Coffee,
            Icons.Outlined.Search,
            Icons.Outlined.Home,
        )
    val checkedIcons =
        listOf(
            Icons.Filled.Work,
            Icons.Filled.Restaurant,
            Icons.Filled.Coffee,
            Icons.Filled.Search,
            Icons.Filled.Home,
        )
    var selectedIndex by remember { mutableIntStateOf(0) }
    FlowRow(
        Modifier.padding(horizontal = 8.dp).fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
        verticalArrangement = Arrangement.spacedBy(2.dp),
    ) {
        options.forEachIndexed { index, label ->
            ToggleButton(
                checked = selectedIndex == index,
                onCheckedChange = { selectedIndex = index },
                shapes =
                    when (index) {
                        0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                        options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                        else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
                    },
                modifier = Modifier.semantics { role = Role.RadioButton },
            ) {
                Icon(
                    if (selectedIndex == index) checkedIcons[index] else unCheckedIcons[index],
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
                Text(label)
            }
        }
    }
}

VerticalButtonGroupSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun VerticalButtonGroupSample() {
    val options = listOf("Button 1", "Button 2", "Button 3", "Button 4", "Button 5")
    var selectedIndex by remember { mutableIntStateOf(0) }
    Column(verticalArrangement = Arrangement.spacedBy((-6).dp)) {
        options.forEachIndexed { index, label ->
            val shape =
                when (index) {
                    0 ->
                        (ButtonGroupDefaults.connectedMiddleButtonShapes().shape
                                as RoundedCornerShape)
                            .copy(topStart = CornerSize(100), topEnd = CornerSize(100))
                    options.lastIndex ->
                        (ButtonGroupDefaults.connectedMiddleButtonShapes().shape
                                as RoundedCornerShape)
                            .copy(bottomStart = CornerSize(100), bottomEnd = CornerSize(100))
                    else -> ButtonGroupDefaults.connectedMiddleButtonShapes().shape
                }
            ToggleButton(
                checked = selectedIndex == index,
                onCheckedChange = { selectedIndex = index },
                shapes =
                    ToggleButtonDefaults.shapes(
                        shape = shape,
                        checkedShape = ButtonGroupDefaults.connectedButtonCheckedShape,
                    ),
                modifier = Modifier.semantics { role = Role.RadioButton },
            ) {
                Text(label)
            }
        }
    }
}

Last updated: