It's All About (Accessibility) Focus And Compose
Reading time: about 8 minutes
Published

I've seen multiple questions in various Slack communities, Stack Overflow, and other places related to focus on Android and how it doesn't behave as expected. The question typically concerns the use of focusRequester
and then inquires why Talkback or other assistive technologies fail to set focus correctly.
The simple answer is that these APIs are different, and focus behaves differently with accessibility focus and keyboard focus.
In this blog post, I'll discuss the differences and what you can do in different cases. Let's first look at what I mean when I talk about accessibility focus and keyboard focus.
Accessibility Focus
In this blog post, accessibility focus refers to the focus related to screen readers, such as TalkBack. It could include focus for switch access as well, but I'm leaving it out of the scope of this blog post.
When using a screen reader, accessibility focus can be set to any element relevant to the user, such as interactive elements, text, images with text alternatives (content descriptions), and other meaningful elements. The focus indicator can look different on different phones, but here's an example from my phone:
It's set by the system, so the app developer can't edit it, and there shouldn't be any need to do that either.
Keyboard Focus
Keyboard focus, on the other hand, is the focus that interactive elements receive when a user uses a keyboard, D-pad, or other keyboard-emulating device for navigation.
Only interactive elements should be focusable; never, for example, headings or other text. The default focus indicator is the ripple, so it's not that visible:
Later, I will write a blog post about customising and making the focus indicator pass the accessibility legal requirements with color contrast and other requirements.
Making a Component Focusable
So you're creating a custom component, and wondering if it's the kind that should be focusable. The short and simple answer is that every component that the user can interact with via touch input should most likely be focusable. The actual interaction should be handled with accessibility actions and key events, depending on the component.
To put it even more clearly: every button, checkbox, editable text, swipeable element, and graph that has touch input for either/both scrolling or/and data point exploration, should usually be focusable.
However, note that when using Material components, such as buttons and other interactive elements, they are already focusable, so you don't need to add it manually. Adding, for example, a focusable
modifier makes the element appear twice in the focus order.
There are several ways to make a custom interactive element focusable for both screen readers and keyboard navigation.
Using clickable
, toggleable
, or selectable
modifiers. If you're creating a component that the user should be able to either click, toggle, or select, use the respective modifiers. They make the component focusable as well.
Using focusable
-modifier. Sometimes a component needs to be focusable, but it's not clickable, toggleable, or selectable. It might, for example, require custom keyboard shortcuts, such as a chart with touch input that reveals more data with touch gestures. In these cases, using the focusable modifier is the right choice for making the component focusable.
Moving Focus Programmatically to a Component
The next thing we need to discuss is moving the focus programmatically based on the user's actions. For keyboard and keyboard-emulating navigation methods, this is relatively straightforward. For screen readers, it requires a little bit more. Let's first discuss the keyboard focus and then the accessibility focus.
Keyboard Focus
If you want to move keyboard focus from an element when a new screen appears, or on, let's say, a button click, FocusRequester
is your friend.
Here's what using it looks like in code:
@Composable
fun Component() {
val focusRequester = remember { FocusRequester() }
Button(
onClick = { focusRequester.requestFocus() }
) {
Text("Move focus")
}
CustomComponentWereFocusing(
modifier = Modifier
.focusRequester(focusRequester)
.toggleable(...)
)
}
In this example, when the user clicks the Button
, focus moves to the CustomComponentWereFocusing
. We first define the focusRequester
by remembering FocusRequester()
, then set it to the focusRequester
modifier for the CustomComponentWereFocusing
. Finally, we call FocusRequester
's requestFocus()
method on button click.
One important thing to remember here, as with other modifiers, is that the order of modifiers matters. So, when setting the focusRequester modifier, it must be placed before the modifier that adds focusability to the component. Otherwise, it doesn't work at all.
Accessibility Focus
Moving the accessibility focus for screen readers needs a little bit more work. The focusRequester
doesn't work here, so we need to resort to workarounds to accomplish it. I'll share some ideas; the final implementation, naturally, depends on the actual use case.
The first suggestion is that, in some cases, changing the traversal order may be the solution. The following section discusses this topic, so keep reading for tips on how to do that.
Then there's a solution for changing the focused
property on the semantics
modifier. I found this solution from Kotlinlang Slack's Compose channel.
The example from the keyboard navigation section with the ability to focus with a screen reader would look like this:
@Composable
fun Component() {
val isFocused by remember { mutableStateOf(false) }
LaunchedEffect(isFocused) {
if (isFocused) isFocused = false
}
Button(
onClick = { isFocused = true }
) {
Text("Move focus")
}
CustomComponentWereFocusing(
modifier = Modifier
.semantics { focused = isFocused }
.toggleable(...)
)
}
So, here we first define a boolean variable isFocused
, which we remember. Then, we set the focused
property of the semantics
modifier to isFocused
for CustomComponentWereFocusing
. On the button click, we set the isFocused
to true to move the focus to CustomComponentWereFocusing
.
And finally, we have a LaunchedEffect
listening for changes to isFocused
, and if it's true, we set it back to false. It doesn't clear the focus from CustomComponentWereFocusing
, but lets us refocus with a new button click if needed.
Here, again, the order of the modifiers matters - semantics
needs to be before the focusability-applying modifier, which is toggleable
in the example.
Changing Traversal Order
The final topic for this blog post is changing traversal order. Let's say we have a layout with two rows and two columns. The layout looks like this:
The default focus order would be as follows: first top row, then bottom row - meaning First, Third, Second, Fourth. And we want it to be in numerical order.
The solutions for accessibility and keyboard focus are different, so let's discuss them separately, starting with accessibility focus.
Accessibility Focus
For accessibility focus, using the semantics
modifier, isTraversalGroup
, and traversalIndex
is the way to customise the focus order.
First, let's set the isTraversalGroup
property to true
for the parent component wrapping the two rows. Why? It's to create a border for our changes in traversal order. You see, if we don't set it, and set the traversalIndex
to the buttons, they'd be last in the screen's focus order, because their traversal index is greater than 0. Zero is the default traversal index for elements that don't have the traversal index explicitly set. However, if we set the border with isTraversalGroup
, then the changes will only affect the focus order within the traversal group, not the screen's focus order.
So, in code it would look like the following:
Column(
modifier = Modifier
.semantics {
isTraversalGroup = true
}
) {
...
}
And then we set the traversalIndex
for each of the buttons to match the focus order we want to create:
Row(...) {
Button(
modifier = Modifier.semantics {
traversalIndex = 1f
},
...
) { Text("First") }
Button(
modifier = Modifier.semantics {
traversalIndex = 3f
},
...
) { Text("Third") }
}
I've omitted the other row, which contains the "Second" and "Fourth" buttons, from the example, but the idea remains the same.
And that's it. With these changes, the focus order for accessibility focus changes. Let's talk about keyboard focus-related changes next.
Keyboard Focus
For keyboard focus, we first need to create FocusRequester
s for each button with FocusRequester.createRefs()
, and then attach them with focusRequester
modifier, and finally, set the focus order with focusProperties
modifier.
Creating the FocusRequester
s looks like the following code:
val (first, second, third, fourth) = remember {
FocusRequester.createRefs()
}
Next, we set the FocusRequester
s with focusRequester
modifier and the next element to focus on with focusProperties
modifier:
Row(...) {
Button(
modifier = Modifier
.focusRequester(first)
.focusProperties { next = second},
...
) { Text("First") }
Button(
modifier = Modifier
.focusRequester(third)
.focusProperties { next = fourth},
...
) { Text("Third") }
}
I've omitted the second row from the example, but it would be similar.
This way, keyboard and keyboard-emulating device navigating users would first get to the "First" button, then "Second", then "Third", and finally, "Fourth".
Wrapping Up
In this blog post, I've discussed the differences between focusing with screen readers (e.g., TalkBack) and navigating with the keyboard. I've also demonstrated the differences between the two in changing the currently focused element and altering the traversal/focus order.
If you want to read the docs, Modify traversal order is the page to check for accessibility focus, and Focus in Compose summarizes the information about keyboard focus.