Button
Control that triggers an immediate action. Covers text, icon-only, floating, and action-chip presentations, which share one role and differ in where the accessible name comes from.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when the user triggers an immediate action and stays on the current screen (e.g., "Save", "Add to Watchlist", "Play").
- Use when the action is presented as a floating action button, an icon-only control, or an assist or suggestion chip. All four carry
Role.Buttonand no state of their own.
Try a different component when
- Do not use when the control represents an on or off state that persists after the tap (use
button.toggle). - Do not use when the control carries a
selectedstate, such as a filter or a removable token (usechip.filterorchip.input). - Do not use when the control opens a list of commands (use
menu.basic). - Do not use when the control opens a URL or leaves the app (use
link.standalone). - Do not use when the control is a row in a list that navigates elsewhere (use
list-item.basic).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- The control reports
Role.Buttonand a click action. Material'sButton,IconButton,FloatingActionButton,AssistChip, andSuggestionChipare the reference implementations across the presentations this pattern covers; anything else has to set both itself (global.native-first). - The button has an accessible name that describes its purpose or action.
- When the button has visible text, that text serves as the accessible name and no
contentDescriptionis set on it. - An icon-only control takes its name from
contentDescriptionon the control, and theIconinside it carriescontentDescription = null. - When several buttons on one screen share visible text, such as a repeated "Edit" per row, give each a
contentDescriptionthat names what it acts on (e.g., "Edit username"). - When an
ExtendedFloatingActionButtoncollapses to icon-only, set acontentDescriptionthat survives the collapse. The visible text is the name while expanded and disappears while collapsed, so a control named only by its text becomes unnamed. - Set
onClickLabelwhen "Double tap to activate" would not tell the user what happens (e.g.,onClickLabel = "add to watchlist"). This is the only supplementary text Compose exposes, and it completes the sentence TalkBack speaks.IconButtonand the other Material composables here take noonClickLabelparameter. Set the label throughModifier.semantics { onClick(label = "...", action = null) }.
- If the action is unavailable, pass
enabled = falserather than removing the handler, so the control stays in the accessibility tree and reports that it is disabled. - Give a
FloatingActionButtonatraversalIndexor a containing traversal group when it is drawn last but read first, because it sits outside the content flow and composition order will not match what the user sees (global.traversal-order). - Meets the touch target baseline in
global_rules.md(global.touch-target-size). - Meets the focus states baseline in
global_rules.md(global.focus-states).
Donts
Avoid these accessibility and UX barriers.
- Do not build a button from
Row,Box, orImagewithModifier.clickablewhen a Material composable exists. It renders identically and exposes no role, so TalkBack announces content with no indication it can be activated. - Do not include the word "button" in
contentDescription. TalkBack appends the role itself, so the control announces "Save button, button". - Do not set
contentDescriptionon theIconinside a labeled button. The button's own name and the icon's name both reach the merged node, and the control announces twice. - Do not put the result of the action in
contentDescriptionwhen it belongs inonClickLabel. The name says what the control is; the click label says what activating it does. - Do not pass a lambda to
onClick'sactionparameter to attach a label on a component likeIconButtonthat takes noonClickLabeldirectly. The parameter replaces the control's click action in the accessibility tree, so TalkBack triggers the lambda instead of the real handler and the button does nothing, while touch input bypasses semantics and still triggers the real action. Passaction = nullto attach the label without replacing the handler. - Do not disable a button by removing its
onClickor wrapping it in a non-clickable container. Both leave a control that looks disabled and reports nothing, and the second silently drops the 48dp minimum that the Material composable applies only while it owns the callback.
Customizable
Alternatives and options that give the AI agent some room to move.
- Any of the five button skins,
Button,ElevatedButton,FilledTonalButton,OutlinedButton, andTextButton, is acceptable. They share one role and one set of semantics and differ only in container and elevation tokens. - The same holds within each family: the four
IconButtonskins are interchangeable, as are the threeFloatingActionButtonsizes, andAssistChipandSuggestionChipdiffer by usage convention rather than by exposed semantics. onClickLabelis optional. Add it when the action's outcome is not obvious from the name, and omit it when the name already says what happens ("Save", "Delete").
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
@Composable
fun ButtonExamples() {
// Text-only. The visible text is the accessible name; set nothing else.
Button(onClick = { /* save */ }) {
Text("Save")
}
// Icon plus text. The text names the control, so the icon is decorative.
Button(onClick = { /* download */ }) {
Icon(Icons.Filled.Download, contentDescription = null)
Text("Download")
}
// Icon-only. The name moves to the control, and onClickLabel says what happens.
IconButton(
onClick = { /* open settings */ },
modifier = Modifier.semantics { onClick(label = "open settings", action = null) }
) {
Icon(Icons.Filled.Settings, contentDescription = "Settings")
}
// Repeated control. The row's subject goes in the name, or every row reads alike.
IconButton(onClick = { /* edit */ }) {
Icon(Icons.Filled.Edit, contentDescription = "Edit username")
}
// Extended FAB. The text is the name while expanded and gone while collapsed,
// so the contentDescription is what survives.
ExtendedFloatingActionButton(
onClick = { /* compose */ },
expanded = false,
icon = { Icon(Icons.Filled.Add, contentDescription = null) },
text = { Text("New list") },
modifier = Modifier.semantics { contentDescription = "New list" }
)
// Assist chip. Role.Button, no selected state; a chip only in appearance.
AssistChip(
onClick = { /* filter by genre */ },
label = { Text("Comedy") }
)
// Disabled. enabled = false keeps it in the tree and reports the state.
Button(onClick = { /* never fires */ }, enabled = false) {
Text("Submit")
}
}