mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
195
.codex/skills/compose-skill/references/accessibility.md
Normal file
195
.codex/skills/compose-skill/references/accessibility.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 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()`
|
||||
|
||||
```kotlin
|
||||
// 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()` |
|
||||
|
||||
```kotlin
|
||||
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.
|
||||
|
||||
```kotlin
|
||||
// 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)")
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// 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.
|
||||
|
||||
```kotlin
|
||||
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.
|
||||
|
||||
```kotlin
|
||||
// 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.
|
||||
|
||||
```kotlin
|
||||
// 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:
|
||||
|
||||
```kotlin
|
||||
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:
|
||||
|
||||
```kotlin
|
||||
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 `contentDescription` for every meaningful `Image` and `Icon`
|
||||
- Use `mergeDescendants` for logically grouped content
|
||||
- Use `clearAndSetSemantics` when auto-generated text is misleading
|
||||
- Enforce 48dp minimum touch targets on custom interactive elements
|
||||
- Pair color with icons/text for status indicators
|
||||
- Use `MaterialTheme.colorScheme` tokens for contrast-safe colors
|
||||
- Test with a screen reader on each target platform
|
||||
|
||||
### Don't
|
||||
|
||||
- Leave `contentDescription = null` on meaningful images without a comment
|
||||
- Apply `role` manually 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
|
||||
Reference in New Issue
Block a user