Accessibility is not optional. Over one billion people worldwide live with some form of disability. Many more experience temporary or situational impairments—a broken arm, bright sunlight obscuring a screen, or holding a baby while trying to use a phone. Building accessible applications ensures everyone can use your software, expands your potential user base, and in many jurisdictions, fulfills legal requirements.
Android provides comprehensive accessibility support through services like TalkBack, Switch Access, and voice control. This guide covers implementing accessibility correctly, from semantic markup through testing with real assistive technologies.
Understanding Assistive Technologies
TalkBack is Android’s screen reader, used by people with visual impairments. It announces UI elements audibly and provides gesture-based navigation. Users explore screens by touch, hearing descriptions of elements under their finger, then double-tap to activate.
Switch Access enables device control through one or more switches—physical buttons, keyboard keys, or head movements detected by camera. Users scan through interactive elements and activate their chosen switch to select.
Voice Access allows hands-free device control through spoken commands. Users speak element labels or numbers displayed on screen to interact with applications.
Magnification helps users with low vision by enlarging portions of the screen. Font scaling increases text size system-wide. Color correction and inversion assist users with color vision deficiencies.
Semantic Content Descriptions
Every interactive element needs a content description that conveys its purpose. Screen readers announce this text to users who cannot see the visual presentation.
// ImageButton needs content description
<ImageButton
android:id="@+id/addToCartButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_cart_add"
android:contentDescription="@string/add_to_cart" />
// Decorative images should be marked
<ImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:src="@drawable/decorative_banner"
android:importantForAccessibility="no" />
Content descriptions should convey meaning, not appearance. Describe what the element does, not what it looks like. Say “Add to cart” not “Shopping cart icon with plus sign.”
For Jetpack Compose, use the semantics modifier to provide accessibility information:
@Composable
fun ProductCard(
product: Product,
onAddToCart: () -> Unit
) {
Card(
modifier = Modifier.semantics {
// Merge child semantics into single announcement
contentDescription = "${product.name}, ${product.formattedPrice}"
}
) {
// Product content
IconButton(
onClick = onAddToCart,
modifier = Modifier.semantics {
contentDescription = "Add ${product.name} to cart"
}
) {
Icon(Icons.Default.AddShoppingCart, contentDescription = null)
}
}
}
@Composable
fun RatingDisplay(rating: Float) {
Row(
modifier = Modifier.semantics {
contentDescription = "Rating: $rating out of 5 stars"
}
) {
repeat(5) { index ->
Icon(
imageVector = if (index < rating) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = null // Described at row level
)
}
}
}
Focus Management and Navigation
Accessibility services navigate through focusable elements in a logical order. Ensure this order matches visual layout and user expectations.
The traversal order follows the view hierarchy by default. Use android:accessibilityTraversalBefore and android:accessibilityTraversalAfter to customize when default order is illogical.
Group related elements so screen readers announce them together rather than separately. This reduces verbosity and provides better context.
// Group product info into single announcement
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:contentDescription="@{@string/product_info_template(product.name, product.price)}">
<TextView
android:text="@{product.name}"
android:importantForAccessibility="no" />
<TextView
android:text="@{product.formattedPrice}"
android:importantForAccessibility="no" />
</LinearLayout>
In Compose, use Modifier.semantics with mergeDescendants to group related content:
Row(
modifier = Modifier.semantics(mergeDescendants = true) {
// Children's semantics merge into this node
}
) {
Text(product.name)
Spacer(Modifier.width(8.dp))
Text(product.formattedPrice)
}
Touch Target Sizes
Interactive elements need adequate touch targets for users with motor impairments. Android recommends minimum 48dp touch targets. Material Design specifies 48dp as the minimum and recommends 56dp for primary actions.
// Ensure minimum touch target
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="@drawable/ic_close" />
In Compose, use Modifier.sizeIn to enforce minimums while allowing larger sizes:
IconButton(
onClick = onClose,
modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
) {
Icon(Icons.Default.Close, contentDescription = "Close")
}
Color and Contrast
Never convey information through color alone. Users with color vision deficiencies cannot distinguish certain color combinations. Supplement color with text, icons, or patterns.
Maintain sufficient contrast between text and backgrounds. WCAG guidelines require 4.5:1 contrast ratio for normal text and 3:1 for large text. Android Studio's Layout Inspector can measure contrast ratios.
// Bad: Status indicated by color only
<View
android:background="@{order.isPaid ? @color/green : @color/red}" />
// Good: Status indicated by text and color
<TextView
android:text="@{order.isPaid ? @string/paid : @string/unpaid}"
android:textColor="@{order.isPaid ? @color/green : @color/red}" />
Text Scaling
Users can increase system font size for readability. Applications must accommodate larger text without breaking layouts or clipping content.
Use sp units for text sizes to respect user font scaling preferences. Ensure layouts remain functional at maximum font scale (typically 2x default).
Test with large font settings enabled. Fixed-height containers may clip scaled text. Prefer wrap_content heights for text containers.
// Bad: Fixed height clips large text
<TextView
android:layout_height="24dp"
android:textSize="14sp" />
// Good: Height adapts to content
<TextView
android:layout_height="wrap_content"
android:minHeight="24dp"
android:textSize="14sp" />
Custom Views and Controls
Custom views require explicit accessibility implementation. Extend AccessibilityNodeInfo to describe your custom view's role, state, and available actions.
class CustomRatingBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
var rating: Float = 0f
set(value) {
field = value.coerceIn(0f, maxRating.toFloat())
invalidate()
// Announce rating change to accessibility services
announceForAccessibility(context.getString(R.string.rating_changed, field))
}
private val maxRating = 5
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(info)
info.className = RatingBar::class.java.name
info.contentDescription = context.getString(
R.string.rating_description,
rating,
maxRating
)
// Indicate this is adjustable
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS)
if (rating > 0) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction(
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
context.getString(R.string.decrease_rating)
))
}
if (rating < maxRating) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction(
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD,
context.getString(R.string.increase_rating)
))
}
}
override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
return when (action) {
AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> {
rating += 1
true
}
AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> {
rating -= 1
true
}
else -> super.performAccessibilityAction(action, arguments)
}
}
}
Live Regions and Announcements
Dynamic content changes need announcement to screen reader users who cannot see updates visually. Live regions automatically announce content changes.
// Announce content changes
<TextView
android:id="@+id/statusText"
android:accessibilityLiveRegion="polite" />
// assertive interrupts current speech
// polite waits for current speech to complete
For important one-time announcements, use announceForAccessibility:
fun showError(message: String) {
errorView.text = message
errorView.announceForAccessibility(message)
}
Testing Accessibility
Automated testing catches many accessibility issues. Espresso's AccessibilityChecks integration flags common problems during instrumented tests.
@RunWith(AndroidJUnit4::class)
class ProductScreenAccessibilityTest {
@Before
fun setup() {
AccessibilityChecks.enable()
.setRunChecksFromRootView(true)
}
@Test
fun productScreen_meetsAccessibilityGuidelines() {
launchActivity<ProductActivity>()
// Accessibility checks run automatically on interactions
onView(withId(R.id.addToCart)).perform(click())
}
}
Manual testing with TalkBack is essential. Automated tests cannot evaluate whether descriptions make sense or navigation flow is logical. Test every screen by enabling TalkBack and navigating without looking at the screen.
Conclusion
Accessibility implementation requires attention throughout development, not as an afterthought. Design with accessibility in mind from the beginning. Include accessibility testing in your regular QA process. Listen to feedback from users with disabilities.
The effort invested in accessibility benefits everyone. Larger touch targets help users in bumpy vehicles. Good contrast aids visibility in bright sunlight. Semantic structure improves voice control and future AI interfaces. Accessible applications are simply better applications.
At RyuPy, accessibility is a core requirement, not an optional enhancement. We believe technology should serve everyone, and we build our applications accordingly.

0 Comments