Touch Targets, Focus Order, and TalkBack Support

Quick answer: Give every interactive Compose element a visible layout area of at least 48.dp by 48.dp, use Material controls when they fit, and keep the composable and visual order aligned. Let TalkBack and keyboard traversal use Compose defaults first. Add isTraversalGroup, traversalIndex, focusGroup, or focusProperties only after testing shows that a complex layout is read or navigated in the wrong order.

Accessibility is not only a screen-reader label. A control also needs enough room to touch reliably, a predictable place in the navigation sequence, and a complete way to perform its action. Compose provides many of these behaviors automatically, but custom icons, dense lists, and responsive layouts are where gaps appear.

Design for three different paths through the screen

“Focus” can mean different things depending on how someone is using the app. Treat these paths as related, but do not assume that fixing one fixes the others.

PathUser inputWhat must be logical
TouchFinger, stylus, switch-like pointer inputThe entire actionable area is large enough and does not collide with neighbors.
TalkBack traversalScreen-reader gesturesSemantic targets are announced in a meaningful reading order with useful actions.
Keyboard or D-pad focusTab, arrow keys, controllerThe visible focus indicator moves predictably and can reach every relevant action.

The Compose focus guide describes the distinction: Tab navigation is one-dimensional and normally follows composable declaration order, while arrow keys and D-pads move by visual direction. TalkBack follows the semantics tree instead. A custom FocusRequester does not reorder TalkBack by itself, and a semantics traversal hint does not replace keyboard focus behavior.

Reserve a 48dp target, not just a 24dp icon

Android’s Compose accessibility defaults recommends a minimum 48.dp by 48.dp target for anything a person can touch or interact with. The graphic can remain 24dp; the control around it should provide the room.

For an icon-only action, IconButton is usually the right answer. It gives the action a Material-sized target and a clear interaction boundary.

@Composable
fun CloseArticleButton(onClose: () -> Unit) {
    IconButton(onClick = onClose) {
        Icon(
            imageVector = Icons.Outlined.Close,
            contentDescription = stringResource(R.string.close_article),
        )
    }
}

For a genuinely custom target, reserve the space in layout with sizeIn. This is safer than depending on hit-target expansion alone, because neighboring small controls can otherwise have overlapping touch areas.

@Composable
fun RemoveFilterButton(onRemove: () -> Unit) {
    Box(
        modifier = Modifier
            .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
            .clickable(
                onClickLabel = stringResource(R.string.remove_filter),
                role = Role.Button,
                onClick = onRemove,
            ),
        contentAlignment = Alignment.Center,
    ) {
        Icon(
            imageVector = Icons.Outlined.Close,
            contentDescription = null,
            modifier = Modifier.size(24.dp),
        )
    }
}

clickable can expand a small touch target outside a composable’s measured bounds. That is helpful, but it is not a substitute for reserving enough space: the official guidance specifically recommends sizeIn to avoid possible overlap between adjacent hit areas. Use the same principle for trailing list icons, segmented controls, and compact toolbars. Why Modifier Order Matters in Jetpack Compose explains why the order of padding, size, and interaction modifiers changes the final target.

Make one choice one target

Dense settings rows often contain a label and a small checkbox or switch. If tapping either means the same thing, give the parent row the interaction and make the child a visual indicator. This produces one large target and one TalkBack stop.

@Composable
fun SyncSetting(
    enabled: Boolean,
    onEnabledChange: (Boolean) -> Unit,
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .toggleable(
                value = enabled,
                role = Role.Switch,
                onValueChange = onEnabledChange,
            )
            .padding(horizontal = 16.dp, vertical = 12.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(
            text = "Sync on mobile data",
            modifier = Modifier.weight(1f),
        )
        Switch(
            checked = enabled,
            onCheckedChange = null,
        )
    }
}

The toggleable parent owns the interaction; the null child callback prevents two competing targets. The Compose API defaults guide documents this whole-row pattern for selection controls. Use Checkbox, RadioButton, and Switch in Jetpack Compose for the related checkbox and radio-group cases.

Do not combine unrelated actions just to reduce the number of TalkBack stops. A row that opens details plus an independent overflow menu should keep both actions discoverable—or move the secondary action to a clearly named custom accessibility action when that produces a better list-navigation experience.

Let the default reading order win first

TalkBack generally traverses Compose content in expected reading order: left-to-right, then top-to-bottom. The most durable way to preserve that order is simple structure:

  • Declare content in the same order it appears visually.
  • Keep a card’s title, summary, and primary action together when they form one task.
  • Avoid visually moving a control far away from the place where it is declared without testing it.
  • Do not add a semantics node to decorative layout pieces just to influence traversal.

The Compose semantics article explains how merged semantics can turn a meaningful card or button into one announcement. Merging is useful when the children represent one action; it is harmful when it hides independent controls.

Group complex layouts before assigning an order

Use isTraversalGroup when a visual group contains fragments that must be read together before TalkBack moves to the next group. Two columns of sentence fragments are a classic example: without boundaries, TalkBack can read both top lines before either bottom line.

@Composable
fun TwoColumnSummary() {
    Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
        Column(Modifier.semantics { isTraversalGroup = true }) {
            Text("This article explains")
            Text("touch targets and focus.")
        }
        Column(Modifier.semantics { isTraversalGroup = true }) {
            Text("It also explains")
            Text("TalkBack traversal.")
        }
    }
}

This adds a boundary, not a new action. Android’s traversal-order guide notes that scroll containers such as LazyColumn and Material surfaces are traversal groups by default, so do not add groups automatically around every list item.

If grouping still cannot express the intended order—such as a clock face or another non-linear layout—use traversalIndex as a precise exception. Lower values are visited first. Set isTraversalGroup = true on the parent and put the indices on selectable semantic children. Hard-coded indices are fragile when items are conditional, reflow on larger screens, or change their z-order, so retest them whenever the layout changes.

Keep keyboard and D-pad focus coherent

For keyboard and D-pad users, Compose defaults are also the starting point. A simple vertical screen should be reachable in its declaration order with Tab and in visual directions with arrows. An off-screen horizontal row of filters is one of the cases where grouping is useful:

Row(
    modifier = Modifier
        .horizontalScroll(rememberScrollState())
        .focusGroup(),
) {
    FilterChip(selected = true, onClick = {}, label = { Text("All") })
    FilterChip(selected = false, onClick = {}, label = { Text("Unread") })
    FilterChip(selected = false, onClick = {}, label = { Text("Saved") })
}

focusGroup() helps Compose navigate through the row’s related focusable children before jumping to a nearby target outside the group. When a two-dimensional layout truly needs a non-default destination, define only that exception with FocusRequester and focusProperties; the focus management and keyboard navigation guide covers that pattern in depth.

Always provide a visible focus indication for custom focusable UI. Material components already have appropriate behavior, but a custom surface should not make keyboard users guess where input will go. Test with a hardware keyboard, D-pad, or controller where the app supports those inputs.

Test the screen with TalkBack, not only semantics assertions

Compose UI tests can confirm that a node has a click action, role, or content description. They cannot tell you whether the sequence feels concise or whether an action is understandable in context. Test an actual route with TalkBack and ask:

  1. Can I swipe forward and backward through primary content in the order I would read it visually?
  2. Does each stop say what it is, its state when relevant, and what activation does?
  3. Can I perform every essential action without a precision gesture?
  4. Are repeated list rows concise, with no duplicate icon or child-control stops?
  5. Can I reach and leave every control with a keyboard or D-pad if the app supports them?

The official Accessibility in Compose codelab recommends TalkBack testing and notes that Accessibility Scanner can flag some issues such as small targets but cannot replace manual testing. Layout Inspector is also useful for inspecting the semantics each composable exposes; Android’s inspect and debug guide lists both tools and the Android Accessibility Suite.

Common mistakes

Making the icon bigger instead of the target

A 24dp icon can stay visually compact inside a 48dp button. Enlarging the symbol alone may disrupt visual hierarchy without creating predictable room around the action.

Relying on invisible hit slop in crowded controls

Compose can expand the hit target of a tiny clickable node, but adjacent expanded areas can become ambiguous. Reserve the minimum size in layout with sizeIn, spacing, or a larger parent action.

Treating TalkBack order and keyboard order as the same API

traversalIndex changes accessibility-service traversal. focusProperties changes keyboard and D-pad focus. Use the one that matches the observed problem, then test the other path too.

Manually ordering ordinary vertical content

Extra indices and focus rules make a screen harder to maintain and can break when content changes. Keep the code and visual structure logical; customize only proven exceptions.

Accessibility navigation checklist

  • Give every interactive element a 48dp minimum layout target.
  • Prefer IconButton, Material controls, and high-level interaction modifiers.
  • Make one user decision one accessible target; keep independent actions independently available.
  • Align declaration, visual, TalkBack, and keyboard order wherever possible.
  • Group semantically related complex content before using explicit traversal indices.
  • Test touch, TalkBack, and keyboard/D-pad navigation on the actual screen.
  • Use Scanner and Layout Inspector as diagnostics, not as a replacement for assistive-technology testing.

The goal is not to force a single navigation model on every screen. It is to let each person reach the same content and actions with a path that is spacious, ordered, and predictable.