We continue our post series where we help you make your apps accessible. In previous chapters, we gave you an overview of what’s coming and shared some tips to start migrating your app. The deadline for making your apps legally accessible is getting closer.
As we’ve mentioned before, accessibility is a long-distance race, and making your app accessible for everyone takes time, patience, and knowledge. But don’t panic — we’re going to share some top tips that will help you tackle this challenge.
This guide is based on the current Android standards as of April 2025, with examples using Kotlin and Compose. If your app includes legacy code, there are also compatible changes for Java and XML.

Now let’s get to it, roll up your sleeves, time to code and tinker, because you know we love it! Apps for everyone? Absolutely. Let’s get to it!
Using ContentDescription
The contentDescription modifier provides support for screen readers such as Google Talkback, allowing us to add a String parameter that describes the Composable element being displayed. It’s especially useful for images, where you can include a description like “deserted tropical beach on a sunny day.”
When the focus lands on the element, the screen reader will read the contentDescription attribute aloud, helping the user understand the content of the element in case of visual impairments. contentDescription values should be simple, direct, and brief, to avoid overwhelming the screen reader with long statements.
Icon(
imageVector = Icons.Default.Close,
contentDescription = “close button”,
tint = contentColor
)
Important: it’s not necessary to describe every element, only the meaningful ones, such as images or icons. If an image is purely decorative, set its description to null to avoid unnecessary noise.
Describe “onClick” actions
Modifier
.clickable(onClickLabel = “View item detail”)
Within the clickable modifier, we have an onClickLabel, which allows us to describe the action performed when tapping on the element, adding semantic meaning to the action, for example, “View item details” or “Add to favorites”.
Not all composable elements include this modifier; in such cases, you can use the “Semantics” modifier to access the click event and set the label there:
Column (
Modifier.semantics {
onClick(label =”add to favorites”, action = addToFavorites)
}
) { ...
Watch out for nested elements
Nesting elements in Compose (or in XML) is extremely common. That’s why you should carefully review gesture-based navigation focus. Some users may rely on adaptive switches, others on TalkBack, and this matters because your interface may not behave as expected, focus could get lost within those elements, semantic messages might be repeated, and interactions may become confusing.
The modifier clearAndSetSemantics allows you to clear internal semantics for that element so that TalkBack only reads the content of that specific element. Keep this in mind if the screen reader is repeating information within the same Composable.
Column(
modifier = modifier
.minimumInteractiveComponentSize()
.clickable(
onClick = onClickMovie
)
.padding(4.dp)
.clearAndSetSemantics {
contentDescription = element.title
}
) {
Card { ...
And remember, not everything needs to be announced, only the important parts. If something doesn’t provide useful information, try to skip it.
Set the “roles” of each element
Within the “semantics” modifier, you can use the “role” modifier to define what type of element you are dealing with. The role tells the screen reader what kind of element currently has focus, such as a switch, checkbox, button, etc.
This is especially useful for custom components, for example, if you have a custom element that behaves like a switch, you can define its behavior for the screen reader with role = Role.Switch.
Let’s look at an example:
Card(onClick = { onOptionToggle() },
modifier = Modifier
.semantics(mergeDescendants = true) {
role = Role.Switch
contentDescription = optionEnabledText
}
.fillMaxWidth()
.minimumInteractiveComponentSize()) {
Row( ...
Here we have a card wrapping our element, which contains a row with the description of our option and its corresponding switch.

For screen readers, we make sure to indicate that the entire card acts as our switch, in order to avoid redundancies.
Handle focus properly
Navigation in Android through certain accessibility tools doesn’t behave the same as normal navigation. Users might navigate with a keyboard, with Voice Access, with a switch device, or through gestures. To move between screen elements, they rely on “next element,” “previous element,” “select element,” and more, which has a significant impact on user experience.
If we have a list of items with nested components, the user might get lost in a sea of focusable elements that provide little to no real value for their interaction.

Hide what doesn’t need focus
Let’s use the previous switch example to illustrate focus handling. We have a card with a nested switch, but we want the parent element to control the toggle. In other words, we tap the card and the switch toggles on or off.
We don’t want the focus to get lost inside each nested element or to land directly on the switch itself. To achieve this, we can hide certain elements from the screen reader using invisibleToUser() inside semantics. This hides the switch from TalkBack, and when combined with Role.Switch, it tells the screen reader that the parent element is the one being announced as a switch.
Card(onClick = { onDarkModeToggle() },
modifier = Modifier
.semantics(mergeDescendants = true) {
role = Role.Switch
}
.fillMaxWidth() {
Row(
modifier = Modifier.fillMaxWidth())
) {
Text(text = “Dark mode”)
Switch(modifier = Modifier
.semantics {
invisibleToUser()
}, checked = isDarkMode, onCheckedChange = { onDarkModeToggle() })
}
}
As you can see, our focus lands where it’s most accessible and doesn’t get lost among its child elements.

And also for the screen reader
If you have an element that doesn’t add value for the screen reader, you can hide it using Modifier.clearAndSetSemantics { … }.
When we nest elements, it’s possible for the screen reader to get confused by reading the semantics of child elements as well, leading to redundant information that doesn’t help the user. To prevent this, we can use clearAndSetSemantics, which disables those details from the child elements, leaving only those of the parent element.
Column(
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable(
onClick = onClickElement
)
.clearAndSetSemantics {
contentDescription = element.title
}
) {
Card {
Image(
modifier = Modifier.semantics {
contentDescription = “Image description”)
...
In this case, we have multiple contentDescription values, one for the parent and one for the child element. This way, the screen reader will only read the parent’s contentDescription. Whether or not to use this modifier depends on the information you want to convey to the user. Each situation will have a more or less optimal solution.
A lot of work. Where do I start?
Don’t worry, we know it’s a lot of changes, many of which aren’t quick, need to be tested properly, and may require coordination with other teams or stakeholders. Our advice: start small, and here are some tips to help:
- Make changes gradually, starting with the easy wins before tackling the more complex ones.
- Test your app using screen readers like TalkBack and interact with it to discover accessibility issues.
- Perform tests with different font sizes.
- Check navigation using voice commands.
- When you're confident in your changes, try to run an accessibility audit.
- Remember that the best testers are often those who experience accessibility challenges firsthand. Keep this in mind when choosing who tests your app.
These are just a few suggestions to help you turn your app into one that works for everyone, but there’s so much more you can do.
We invite you to explore, learn more, and dive into the wonderful world of accessibility so that everyone can access your apps and help make the world more inclusive.
Accessibility is now an essential requirement, and we’re already on board. Are you with us?
References
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.