Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
6.8 KiB
Accessibility
Content Descriptions
Every Image and Icon composable must have an explicit contentDescription:
- Decorative (no information conveyed):
contentDescription = null - Meaningful (conveys information): localized string via
stringResource()
// Decorative — purely visual, screen reader skips it
Icon(Icons.Default.Star, contentDescription = null)
// Meaningful — screen reader announces it
Image(
painter = painterResource(Res.drawable.profile_avatar),
contentDescription = stringResource(Res.string.user_avatar_description)
)
Flag any Image with a non-obvious resource name and contentDescription = null that lacks a comment explaining why it is decorative.
Semantics API
Use Modifier.semantics { } to add or override accessibility information.
| Property | Purpose | Example values |
|---|---|---|
contentDescription |
Override screen reader announcement | "Profile picture of $name" |
role |
Declare interactive role | Role.Button, Role.Image, Role.Switch, Role.Tab, Role.RadioButton, Role.Checkbox |
stateDescription |
Describe current state | "Expanded", "Selected", "3 of 5" |
heading |
Mark as section heading | heading() |
Box(
modifier = Modifier.semantics {
contentDescription = "Profile picture of ${user.name}"
role = Role.Image
}
)
Prefer built-in Material components (Button, Switch, Checkbox) over manual role assignment — they include correct semantics automatically.
Grouping and Overriding Semantics
mergeDescendants
Groups a composable's children into a single screen reader announcement. Use when children together form one logical unit.
// GOOD — screen reader announces "4.5 stars, 128 reviews" as one item
Row(modifier = Modifier.semantics(mergeDescendants = true) { }) {
Icon(Icons.Default.Star, contentDescription = null)
Text("4.5 stars")
Text("(128 reviews)")
}
// BAD — screen reader stops on each child separately, fragmenting the meaning
Row {
Icon(Icons.Default.Star, contentDescription = "Star icon")
Text("4.5 stars")
Text("(128 reviews)")
}
clearAndSetSemantics
Replaces all auto-generated and child semantics with a single custom description. Use when the auto-generated text is verbose or misleading.
Row(modifier = Modifier.clearAndSetSemantics {
contentDescription = "Rating: 4.5 stars from 128 reviews"
}) {
StarRating(4.5f)
Text("(128 reviews)")
}
| Need | Use |
|---|---|
| Group children into one announcement, keep their text | semantics(mergeDescendants = true) |
| Replace all child semantics with a custom string | clearAndSetSemantics { } |
Touch Targets
Minimum interactive size: 48 x 48 dp.
- Use
Modifier.minimumInteractiveComponentSize()on custom interactive elements to enforce this automatically. - Material components (
Button,IconButton,Switch, etc.) handle this internally — do not add redundant padding.
// Custom clickable element — enforce minimum touch target
Box(
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable { onAction() }
) {
Icon(Icons.Default.Add, contentDescription = "Add item")
}
Color and Contrast
WCAG AA minimum contrast ratios:
| Text type | Minimum ratio |
|---|---|
| Normal text (<18sp) | 4.5 : 1 |
| Large text (18sp+ or 14sp bold+) | 3 : 1 |
Never use color as the only way to convey information. Always pair with an icon, text label, or pattern.
// BAD — only color differentiates status
Box(modifier = Modifier.background(if (isOnline) Color.Green else Color.Red))
// GOOD — icon + text + color
Row {
Icon(
imageVector = if (isOnline) Icons.Default.CheckCircle else Icons.Default.Cancel,
contentDescription = null,
)
Text(if (isOnline) "Online" else "Offline")
}
Use theme tokens (MaterialTheme.colorScheme) rather than hardcoded colors — theme tokens are designed to meet contrast requirements across light/dark modes.
Custom Interactive Elements
When using Modifier.clickable on a non-Button composable, add semantic role and click label:
Card(
modifier = Modifier
.clickable(onClickLabel = "Open book details") { onBookClick(book.id) }
.semantics { role = Role.Button }
) {
Text(book.title)
}
Prefer Button / IconButton / TextButton over custom clickable elements when possible — they include correct semantics, touch targets, and visual feedback out of the box.
Custom Accessibility Actions
For composables with multiple actions (e.g., a list item with favorite, share, delete), expose named accessibility actions so screen reader users can discover and invoke them without navigating to individual buttons:
Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction("Add to favorites") { onFavorite(); true },
CustomAccessibilityAction("Share") { onShare(); true },
)
}
The lambda returns true if the action was handled successfully.
MVI Integration
Accessibility does not change the MVI architecture. Key placement rules:
| Concern | Where | Why |
|---|---|---|
Semantic descriptions (contentDescription, stateDescription) |
Screen / Leaf composables | These are UI-layer concerns — resolve from state close to rendering |
| Semantic keys/enums for dynamic descriptions | State data class |
e.g., statusLabel: StringKey — the UI resolves to a localized string |
Modifier.semantics |
Composable modifier chains |
Applied in the UI layer, never in ViewModel |
| Accessibility-triggered actions (e.g., custom action callbacks) | onEvent callbacks → ViewModel |
Same as any user interaction — goes through the event pipeline |
Keep accessibility descriptions in the UI layer, not in state. State holds semantic keys (enums, string resource keys); the Screen/Leaf composable resolves them to localized strings via stringResource().
Do / Don't
Do
- Provide
contentDescriptionfor every meaningfulImageandIcon - Use
mergeDescendantsfor logically grouped content - Use
clearAndSetSemanticswhen auto-generated text is misleading - Enforce 48dp minimum touch targets on custom interactive elements
- Pair color with icons/text for status indicators
- Use
MaterialTheme.colorSchemetokens for contrast-safe colors - Test with a screen reader on each target platform
Don't
- Leave
contentDescription = nullon meaningful images without a comment - Apply
rolemanually when a Material component already provides it - Add extra padding on Material components that already meet touch target requirements
- Rely on color alone to communicate state changes
- Put localized accessibility strings in ViewModel state — use semantic keys and resolve in UI
- Hardcode accessibility text — use
stringResource()for localization