Skip to content
Home
← Back to BlogDesign
2025-12-17
7 min read

Mastering Accessibility (RGAA 4.1) with Jetpack Compose: The Ultimate Guide

Best practices for creating compose components that are accessible to everyone.

AccessibilityJetpack ComposeRGAA 4.1
Share:

Introduction

With the tightening of European standards (and the strict application of RGAA 4.1 in France), digital accessibility is no longer an "optional feature." It is a legal requirement and an ethical necessity.

As Android developers, we have a responsibility to leave no one behind. Fortunately, Jetpack Compose makes this task significantly easier thanks to its native semantics system.

In this guide, we will explore how to build 100% accessible apps, from basic concepts to advanced component implementation and testing strategies.


1. What is Accessibility?

Simply put, accessibility (a11y) means removing barriers. It is the art of designing an application that everyone can perceive, understand, navigate, and interact with, regardless of their physical or cognitive abilities.

To comply with RGAA 4.1 (aligned with WCAG 2.1), we must support these primary user profiles:

  • Visual Impairment: Blind users (relying on screen readers like TalkBack), low-vision users (needing high contrast and large text), or colorblind users.
  • Motor Impairment: Users with tremors, limited dexterity, or those using switch access devices who need large touch targets.
  • Cognitive Impairment: Users who need clear, predictable interfaces without unnecessary distractions.
  • Auditory Impairment: Users who need captions or visual alternatives to sound.

2. Practical Implementation with Jetpack Compose

Compose generates a Semantics Tree that runs parallel to the UI Tree. This is what accessibility services (like TalkBack) read to understand your app.

Here is how to handle specific cases to ensure compliance.

Case A: Visual Elements (For Screen Readers)

The most common issue is the lack of text descriptions for graphic elements.

1. Images and Icons

Rule: If an image provides information, it must be described. If it is purely decorative, it must be explicitly ignored by the accessibility service.

kotlin
1// ❌ Bad Practice
2Image(
3 painter = painterResource(id = R.drawable.ic_logo),
4 contentDescription = null // TalkBack might say "Unlabelled image" or ignore it unpredictably
5)
6
7// βœ… Good Practice (Informative)
8Image(
9 painter = painterResource(id = R.drawable.ic_profile),
10 contentDescription = "User profile photo" // Read aloud by TalkBack
11)
12
13// βœ… Good Practice (Decorative)
14Image(
15 painter = painterResource(id = R.drawable.bg_pattern),
16 contentDescription = null // Explicit null tells Compose to skip this element entirely
17)

2. Custom Components

If you create a Box that acts like a button, TalkBack won't know it's interactive unless you define its role.

kotlin
1Box(
2 modifier = Modifier
3 .clickable(onClick = { /* action */ })
4 .semantics {
5 role = Role.Button // Tells TalkBack: "This is a Button"
6 contentDescription = "Add to cart"
7 // For states (checked/unchecked, enabled/disabled)
8 stateDescription = if (isAdded) "Added" else "Not added"
9 }
10) {
11 // Visual content here
12}

Case B: Readability and Text (Low Vision)

1. Text Scaling

Rule: Never use dp for text size. Always use sp. Users must be able to scale text up to 200% via system settings without breaking the UI.

kotlin
1Text(
2 text = "Welcome",
3 fontSize = 18.sp // βœ… Adapts to user preferences
4 // fontSize = 18.dp ❌ Fixed size, non-compliant with RGAA
5)

2. Color Contrast

The contrast ratio between text and background must be at least 4.5:1 for small text and 3.0:1 for large text.

Compose Tip: Utilize MaterialTheme, which generally handles contrast well by default, but always double-check custom color palettes.

Case C: Motor Impairment (Touch Targets)

Minimum Touch Target Size

Standard: An interactive element must measure at least 48dp x 48dp.

If your icon is only 24dp, you should not necessarily make the icon bigger, but you must expand its "clickable" area.

kotlin
1IconButton(
2 onClick = { /* ... */ },
3 modifier = Modifier
4 // Ensures the touch area is at least 48dp, even if the icon is smaller
5 .minimumInteractiveComponentSize()
6) {
7 Icon(
8 imageVector = Icons.Default.Add,
9 contentDescription = "Add item"
10 )
11}

Case D: Navigation Order

By default, TalkBack reads from top to bottom, left to right. Sometimes, the visual design does not match the logical reading order.

kotlin
1Column(
2 // Forces TalkBack to read this group logically as a unit or in a specific order
3 modifier = Modifier.semantics { isTraversalGroup = true }
4) {
5 Text(
6 text = "Important Title",
7 modifier = Modifier.semantics { traversalIndex = 0f } // Read first
8 )
9 Text(
10 text = "Secondary Detail",
11 modifier = Modifier.semantics { traversalIndex = 1f } // Read second
12 )
13}

3. Advanced Scenario: The "Selection Card"

Let's look at a complex, real-world example. Imagine a subscription card (Standard vs. Premium) containing a title, price, and description.

The Problem: If you just use a Card with clickable, TalkBack will read every text element individually via swiping, without indicating that the card is a selectable option or its current state.

The Solution:

kotlin
1@Composable
2fun SubscriptionOptionCard(
3 title: String,
4 price: String,
5 description: String,
6 isSelected: Boolean,
7 onOptionSelected: () -> Unit
8) {
9 Surface(
10 shape = RoundedCornerShape(8.dp),
11 border = BorderStroke(
12 width = if (isSelected) 2.dp else 1.dp,
13 color = if (isSelected) MaterialTheme.colors.primary else Color.Gray
14 ),
15 modifier = Modifier
16 .fillMaxWidth()
17 .padding(8.dp)
18 // --- ACCESSIBILITY MAGIC STARTS HERE ---
19 .selectable(
20 selected = isSelected,
21 onClick = onOptionSelected,
22 role = Role.RadioButton // 1. Define the Role
23 )
24 .semantics(mergeDescendants = true) {
25 // 2. Merge descendants so the whole card is read as one item
26
27 // 3. Custom state description (optional but helpful)
28 stateDescription = if (isSelected) "Option selected" else "Option not selected"
29 }
30 // --- END ---
31 ) {
32 Column(modifier = Modifier.padding(16.dp)) {
33 Text(text = title, style = MaterialTheme.typography.h6)
34 Spacer(modifier = Modifier.height(4.dp))
35 Text(text = price, style = MaterialTheme.typography.h5, fontWeight = FontWeight.Bold)
36 Spacer(modifier = Modifier.height(8.dp))
37 Text(text = description, style = MaterialTheme.typography.body2)
38 }
39 }
40}

Why this works:

  1. Role.RadioButton: TalkBack announces "Radio button, 1 of 2".
  2. mergeDescendants = true: TalkBack reads the title, price, and description in one clear sentence instead of requiring 3 swipes.
  3. stateDescription: Provides explicit context.

4. Testing Strategy: Ensuring 100% Compliance

Coding is not enough; validation is key. Use this three-pillar strategy.

Phase 1: Unit Tests (Automated)

Verify semantic attributes without launching the full app.

kotlin
1@Test
2fun testButtonHasClickActionAndDescription() {
3 composeTestRule.setContent {
4 MyCustomButton()
5 }
6
7 composeTestRule
8 .onNodeWithContentDescription("Submit") // Check description
9 .assertHasClickAction() // Check clickability
10 .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button)) // Check Role
11}

Phase 2: Instrumented Tests (Automated)

Use Espresso and the Google Accessibility framework to scan screens during integration tests.

kotlin
1@Test
2fun accessibilityCheck() {
3 AccessibilityChecks.enable() // Enables automatic contrast and touch target checking
4
5 composeTestRule.setContent {
6 MyScreen()
7 }
8
9 // The test will fail here if RGAA violations are detected
10 // (e.g., low contrast or small touch target)
11}

Phase 3: Manual Tests (Essential)

Automated tools only catch about 30-50% of errors. Humans must do the rest.

  1. Accessibility Scanner App: Download Google's official app. It takes a snapshot of your screen and highlights errors (orange boxes) regarding contrast or touch targets.
  2. TalkBack Navigation: Close your eyes. Enable TalkBack. Try to use your main feature.
    • Can I navigate?
    • Do I know where I am?
    • Can I go back?

Conclusion: Building Bridges, Not Walls

Applying RGAA 4.1 via Jetpack Compose might seem like an extra technical constraint, a list of boxes to tick in a Jira ticket. But in reality, it is the essence of our job.

As developers, we build digital tools. If these tools exclude 15% to 20% of the population because of poor contrast or an unlabeled button, we have failed in our primary mission: connecting people.

The adoption of Jetpack Compose is a golden opportunity. Google has given us powerful semantic tools that make accessibility almost native, provided we think about it from the very first line of code.

So, the next time you write a modifier, ask yourself: "If I close my eyes, does this code still make sense?" If the answer is yes, you are not just respecting a European standard; you are opening your application to the whole world.

Happy coding! πŸš€

Did you find this helpful?

Comments

Related Posts