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

SplitButtonLayout

Common

Component in Material 3 Compose

A [SplitButtonLayout] let user define a button group consisting of 2 buttons. The leading button performs a primary action, and the trailing button performs a secondary action that is contextually related to the primary action.

@sample androidx.compose.material3.samples.FilledSplitButtonSample @sample androidx.compose.material3.samples.TonalSplitButtonSample @sample androidx.compose.material3.samples.ElevatedSplitButtonSample @sample androidx.compose.material3.samples.OutlinedSplitButtonSample @sample androidx.compose.material3.samples.SplitButtonWithUnCheckableTrailingButtonSample @sample androidx.compose.material3.samples.SplitButtonWithTextSample @sample androidx.compose.material3.samples.SplitButtonWithIconSample

Choose the best split button for an action based on the amount of emphasis it needs. The more important an action is, the higher emphasis its button should be.

Use [SplitButtonDefaults.LeadingButton] and [SplitButtonDefaults.TrailingButton] to construct a FilledSplitButton. Filled split button is the high-emphasis version of split button. It should be used for emphasizing important or final actions.

Use [SplitButtonDefaults.TonalLeadingButton] and [SplitButtonDefaults.TonalTrailingButton] to construct a tonal SplitButton. Tonal split button is the medium-emphasis version of split buttons. It's a middle ground between filled SplitButton and outlined SplitButton

Use [SplitButtonDefaults.ElevatedLeadingButton] and [SplitButtonDefaults.ElevatedTrailingButton] to construct a elevated SplitButton. Elevated split buttons are essentially tonal SplitButtons with a shadow. To prevent shadow creep, only use them when absolutely necessary, such as when the button requires visual separation from patterned container.

Use [SplitButtonDefaults.OutlinedLeadingButton] and [SplitButtonDefaults.OutlinedTrailingButton] to construct a outlined SplitButton. Outlined split buttons are medium-emphasis buttons. They contain actions that are important, but are not the primary action in an app. Outlined buttons pair well with filled SplitButtons to indicate an alternative, secondary action.

Last updated:

Installation

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

Overloads

@ExperimentalMaterial3ExpressiveApi
@Composable
fun SplitButtonLayout(
    leadingButton: @Composable () -> Unit,
    trailingButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    spacing: Dp = SplitButtonDefaults.Spacing,
)

Parameters

namedescription
leadingButtonthe leading button. You can specify your own composable or construct a [SplitButtonDefaults.LeadingButton]
trailingButtonthe trailing button.You can specify your own composable or construct a [SplitButtonDefaults.TrailingButton]
modifierthe [Modifier] to be applied to this split button.
spacingThe spacing between the [leadingButton] and [trailingButton]

Code Examples

FilledSplitButtonSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun FilledSplitButtonSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.LeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.TrailingButton(
                checked = checked,
                onCheckedChange = { checked = it },
                modifier =
                    Modifier.semantics {
                        stateDescription = if (checked) "Checked" else "Unchecked"
                        contentDescription = "Toggle Button"
                    },
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

TonalSplitButtonSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun TonalSplitButtonSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.TonalLeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.TonalTrailingButton(
                checked = checked,
                onCheckedChange = { checked = it },
                //                modifier =
                //                    Modifier.semantics {
                //                        stateDescription = if (checked) "Checked" else "Unchecked"
                //                        contentDescription = "Toggle Button"
                //                    },
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

ElevatedSplitButtonSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun ElevatedSplitButtonSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.ElevatedLeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.ElevatedTrailingButton(
                checked = checked,
                onCheckedChange = { checked = it },
                modifier =
                    Modifier.semantics {
                        stateDescription = if (checked) "Checked" else "Unchecked"
                        contentDescription = "Toggle Button"
                    },
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

OutlinedSplitButtonSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun OutlinedSplitButtonSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.OutlinedLeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.OutlinedTrailingButton(
                checked = checked,
                onCheckedChange = { checked = it },
                modifier =
                    Modifier.semantics {
                        stateDescription = if (checked) "Checked" else "Unchecked"
                        contentDescription = "Toggle Button"
                    },
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

SplitButtonWithUnCheckableTrailingButtonSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun SplitButtonWithUnCheckableTrailingButtonSample() {
    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.LeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                    contentDescription = "Localized description",
                )
                Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.TrailingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier = Modifier.size(SplitButtonDefaults.TrailingIconSize),
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

SplitButtonWithTextSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun SplitButtonWithTextSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.LeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Text("My Button")
            }
        },
        trailingButton = {
            SplitButtonDefaults.TrailingButton(
                checked = checked,
                onCheckedChange = { checked = it }
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}

SplitButtonWithIconSample

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@Preview
fun SplitButtonWithIconSample() {
    var checked by remember { mutableStateOf(false) }

    SplitButtonLayout(
        leadingButton = {
            SplitButtonDefaults.LeadingButton(
                onClick = { /* Do Nothing */ },
            ) {
                Icon(
                    Icons.Filled.Edit,
                    contentDescription = "Localized description",
                    Modifier.size(SplitButtonDefaults.LeadingIconSize)
                )
            }
        },
        trailingButton = {
            SplitButtonDefaults.TrailingButton(
                checked = checked,
                onCheckedChange = { checked = it }
            ) {
                val rotation: Float by
                    animateFloatAsState(
                        targetValue = if (checked) 180f else 0f,
                        label = "Trailing Icon Rotation"
                    )
                Icon(
                    Icons.Filled.KeyboardArrowDown,
                    modifier =
                        Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                            this.rotationZ = rotation
                        },
                    contentDescription = "Localized description"
                )
            }
        }
    )
}
by @alexstyl