Back to all articles
ProgrammingBy Dhanasekaran7 min readJuly 2, 2026

Building Accessible Android Apps with Jetpack Compose: A Developer's Practical Guide

Make your Android app accessible with Jetpack Compose. Covers semantics, contrast, focus order, custom composables, and automated testing tools.

Building Accessible Android Apps with Jetpack Compose: A Developer's Practical Guide

If you've ever run TalkBack on your own app and cringed at what you heard, you already know why accessibility matters. It's not a checkbox for the Play Store or a nice-to-have for the "someday" backlog — it's a core part of shipping software that actually works for the people using it.

This guide walks through the practical, code-level things you can do to make an Android app accessible, along with the tools that make auditing painless.

Why This Isn't Optional Anymore

Over a billion people worldwide live with some form of disability. On top of that, Google now factors accessibility into Play Store quality signals, and several countries have started tying app accessibility to legal compliance (ADA in the US, EN 301 549 in the EU). Beyond the compliance angle, accessible apps are just better apps — larger touch targets, clearer navigation, and better contrast tend to improve usability for everyone, not just users with disabilities.

The Core Building Blocks

1. Content Descriptions via Semantics

Every interactive or meaningful visual element needs a description so screen readers like TalkBack can announce it. In Compose, this goes through the semantics modifier, or the built-in parameter most components already expose:

Icon(
    imageVector = Icons.Filled.Star,
    contentDescription = stringResource(R.string.add_to_favorites),
    modifier = Modifier.clickable { onFavoriteClick() }
)

For custom composables without a built-in contentDescription parameter, use Modifier.semantics:

Box(
    modifier = Modifier.semantics {
        contentDescription = "Add to favorites"
        role = Role.Button
    }
)

Decorative images that carry no information should explicitly opt out so TalkBack skips them:

Image(
    painter = painterResource(R.drawable.decorative_banner),
    contentDescription = null
)

A common mistake: hardcoding the description as a string literal instead of stringResource. This breaks localization and is an easy flag in any accessibility review.

2. Touch Target Size

Android's accessibility guidelines call for a minimum touch target of 48x48dp. Material components in Compose (IconButton, Button, etc.) already enforce this via minimumInteractiveComponentSize(), but custom clickable composables don't get it for free:

Box(
    modifier = Modifier
        .size(24.dp) // visual icon size
        .minimumInteractiveComponentSize() // pads the tappable area to 48dp
        .clickable { onDelete() }
) {
    Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
}

If you're not using a Material component and want full manual control, just set the size explicitly and use padding to keep the visual icon smaller than the tap area:

Icon(
    imageVector = Icons.Filled.Delete,
    contentDescription = stringResource(R.string.delete),
    modifier = Modifier
        .size(48.dp)
        .padding(12.dp)
        .clickable { onDelete() }
)

3. Color Contrast

WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. This is easy to check programmatically or with tooling — don't rely on eyeballing it, since what looks fine on your calibrated monitor may fail on a mid-range device in sunlight.

4. Focus Order and Navigation

TalkBack and switch-access users move through the screen in a specific order, which by default follows composition order. If you use custom layouts (Layout, absolute offsets, or reordering with zIndex), that traversal order can end up scrambled relative to what's visually on screen. Compose exposes traversalIndex in the semantics block to override it explicitly:

Text(
    text = "Email",
    modifier = Modifier.semantics { traversalIndex = 0f }
)
TextField(
    value = email,
    onValueChange = { email = it },
    modifier = Modifier.semantics { traversalIndex = 1f }
)

Lower values are visited first. This is worth checking any time a layout deviates from a simple top-to-bottom Column.

5. Live Regions for Dynamic Content

If your UI updates without user interaction (a loading state resolving, an error appearing), mark it as a live region so screen readers announce the change automatically:

Text(
    text = errorMessage,
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

Use LiveRegionMode.Assertive sparingly — only for urgent, interrupting information like a failed payment, since it interrupts whatever TalkBack is currently announcing.

6. Custom Gesture-Based Composables Need Manual Work

If you're building something with Modifier.pointerInput or a custom drawBehind component instead of standard clickable elements, accessibility doesn't come for free — TalkBack has no way to know the element is interactive or what it does. Describe the role, state, and available actions explicitly:

Box(
    modifier = Modifier
        .pointerInput(Unit) { detectTapGestures { onPlayClick() } }
        .semantics {
            role = Role.Button
            contentDescription = "Play video"
            onClick(label = "play") {
                onPlayClick()
                true
            }
        }
)

Wiring onClick into the semantics block (rather than only relying on the gesture detector) matters because it lets TalkBack and switch-access users trigger the action through their own input method, not just a raw tap.

Testing: Manual and Automated

Manual Testing with TalkBack

There's no substitute for actually turning on TalkBack (Settings → Accessibility → TalkBack) and navigating your own app with your eyes closed, or at least without looking at the screen. It surfaces issues no static analysis catches — awkward reading order, missing state announcements, confusing focus jumps.

Automated Auditing

Two tools are worth building into your workflow:

  • Accessibility Scanner (Google's own app) gives you a quick on-device pass with suggestions on touch target size, contrast, and missing labels.
  • uiautomator dump lets you programmatically extract the accessibility tree of any screen as XML, which is powerful if you want to build custom, rule-based audits across many screens or app states rather than checking one screen at a time by hand.
  • BrowserStack App Accessibility is worth adopting if you need repeatable, documented scan results across devices — especially useful when you need to produce a report for stakeholders or compliance sign-off rather than just fixing issues ad hoc.

A practical pattern that scales well: run automated scans to catch the mechanical issues (missing labels, contrast, touch targets), then reserve manual TalkBack testing for flows and interactions that automated tools structurally can't evaluate — like whether the reading order actually makes sense.

A Simple Pre-Release Checklist

  • [ ] Every interactive element has a meaningful contentDescription, or is explicitly marked decorative
  • [ ] Touch targets are at least 48x48dp
  • [ ] Text meets 4.5:1 contrast ratio (3:1 for large text)
  • [ ] Focus order matches visual/logical order
  • [ ] Dynamic content changes are announced via live regions
  • [ ] Custom gesture-based composables expose role, state, and actions via Modifier.semantics
  • [ ] App has been navigated end-to-end with TalkBack, not just scanned

Closing Thought

Accessibility work has a reputation for being tedious, but most of it is a handful of habits — labeling, sizing, contrast, and testing with a screen reader — applied consistently. Once it's part of your normal review process rather than a separate pass at the end, it stops being a burden and starts being just how you build.


Let's Build Your Product the Right Way

I'm Dhanasekaran, a Full Stack Engineer based in India with 7+ years of experience building production-grade web and mobile applications — using Next.js, Node.js, React Native, and Kotlin, all structured with Clean Architecture principles.

Whether you're starting from scratch or need to rescue an existing project, I'd love to discuss your needs.

👉 View My Services on Fiverr

Or connect with me directly on LinkedIn to talk about your project.


Dhanasekaran is a Full Stack Engineer with 7+ years of experience in web and mobile development. He works with founders and businesses to build scalable, maintainable software products from the ground up.

Published on Mister.D platform👁️ 5 views
Building Accessible Android Apps with Jetpack Compose: A Developer's Practical Guide | Mister.D