--- name: raam-code description: > Develop accessible mobile applications (iOS and Android) conforming to RAAM 1.1 (Luxembourg Mobile Accessibility Assessment Framework, based on EN 301 549 v3.2.1 / WCAG 2.1). Use when building native mobile apps, React Native, Flutter, or any mobile UI that must meet Luxembourg accessibility law. Covers all 15 RAAM themes with platform-specific code guidance. Default conformance target: Level AA. metadata: author: luxembourg-accessibility-skillset version: 1.0.0 raam-version: "1.1" wcag-version: "2.1" en301549-version: "3.2.1" license: CC-BY-3.0-LU source: https://github.com/accessibility-luxembourg/ReferentielAccessibiliteMobile allowed-tools: Bash Read Grep --- # RAAM 1.1 — Accessible Mobile Development Guide You are an accessibility-aware mobile developer. Every piece of iOS, Android, React Native, or Flutter code you write MUST conform to **RAAM 1.1** (Level AA by default). RAAM is Luxembourg's official mobile accessibility assessment framework implementing EN 301 549 v3.2.1 and WCAG 2.1. ## How to use the reference data The full RAAM 1.1 criteria, test methodologies, and glossary are available as JSON files. Use the lookup script to query specific criteria on demand: ```bash # List all topics !`${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topics` # Look up a specific criterion bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh criterion 9.1 # Look up test methodology bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh methodology 9.1 # Search criteria by keyword bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh search "form" # Check glossary definition bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh glossary "assistive technologies" ``` The raw JSON reference files are located at: - `${CLAUDE_SKILL_DIR}/../references/raam/criteres.json` — All 108 criteria with tests, levels, and EN 301 549 mappings - `${CLAUDE_SKILL_DIR}/../references/raam/methodologies.json` — Step-by-step test procedures (iOS & Android) - `${CLAUDE_SKILL_DIR}/../references/raam/glossaire.json` — Glossary of mobile accessibility terms When writing code for a specific component, ALWAYS look up the relevant RAAM criteria first. For example, before writing a form, run `search "form"` and `topic 9`. --- ## Core rules (apply to ALL mobile code you write) ### 1. Graphic Elements (Topic 1) **ALWAYS:** - Every decorative graphic element MUST be ignored by assistive technologies (1.1) - Every informative graphic element MUST have an accessible alternative (1.2) - Text alternatives must be relevant — describe what the element conveys (1.3) - CAPTCHA graphic elements must describe their nature/function, not the answer (1.4) - Complex graphics (charts, maps) need a detailed description (1.6, 1.7) - Avoid text in graphic elements unless the effect cannot be reproduced with styled text (1.8 — Level AA) **iOS (SwiftUI):** ```swift // GOOD: informative image Image("chart-sales") .accessibilityLabel("Sales increased 40% in Q3 2024") // GOOD: decorative image — hidden from VoiceOver Image("decorative-wave") .accessibilityHidden(true) // GOOD: icon button with accessibility Button(action: { /* ... */ }) { Image(systemName: "magnifyingglass") } .accessibilityLabel("Search") ``` **iOS (UIKit):** ```swift // Informative image imageView.isAccessibilityElement = true imageView.accessibilityLabel = "Sales increased 40% in Q3 2024" // Decorative image decorativeImageView.isAccessibilityElement = false decorativeImageView.accessibilityElementsHidden = true ``` **Android (Jetpack Compose):** ```kotlin // GOOD: informative image Image( painter = painterResource(R.drawable.chart_sales), contentDescription = "Sales increased 40% in Q3 2024" ) // GOOD: decorative image Image( painter = painterResource(R.drawable.decorative_wave), contentDescription = null // null = decorative in Compose ) // GOOD: icon button IconButton(onClick = { /* ... */ }) { Icon( Icons.Default.Search, contentDescription = "Search" ) } ``` **Android (XML Views):** ```xml ``` **React Native:** ```jsx // Informative image // Decorative image ``` **Flutter:** ```dart // Informative image Image.asset( 'assets/chart.png', semanticsLabel: 'Sales increased 40% in Q3 2024', ) // Decorative image Semantics( excludeSemantics: true, child: Image.asset('assets/decoration.png'), ) ``` ### 2. Colours (Topic 2) - Information MUST NOT be conveyed by colour alone — always add shape, text, or icon (2.1) - Text contrast: at least **4.5:1** (normal text) or **3:1** (large text ≥24px / bold ≥18.5px) (2.2 — Level AA) - Non-text element contrast (icons, borders, UI controls): at least **3:1** (2.3 — Level AA) - If contrast is insufficient by default, a replacement mechanism must exist and itself be accessible (2.4 — Level AA) **iOS:** ```swift // GOOD: support system contrast settings // Use semantic colours that adapt to Increase Contrast mode Text("Important") .foregroundColor(.primary) // adapts to accessibility settings // Support Differentiate Without Colour HStack { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("Validated") // text reinforces the green colour meaning } ``` **Android:** ```kotlin // Support high contrast text mode // Use theme colours that respond to system accessibility settings Text( text = "Important", color = MaterialTheme.colorScheme.onSurface // adapts to theme ) ``` ### 3. Multimedia (Topic 3) - Audio-only media MUST have a text transcript (3.1, 3.2) - Video-only media MUST have a text transcript, audio description, or audio-only alternative (3.3, 3.4) - Synchronised media MUST have captions (3.7, 3.8) - Synchronised media MUST have audio description (3.9, 3.10 — Level AA) - Media MUST be identified by an adjacent text label outside the player (3.11) - Auto-playing audio must last ≤3 seconds or provide a stop/mute control (3.12) - Media player MUST provide: play, pause/stop, mute, and caption/AD toggles (3.13) - Caption and AD controls must be at the same level as play/pause (3.14 — Level AA) ### 4. Tables (Topic 4) - Data tables MUST have headers correctly associated with data cells (4.1, 4.2, 4.3) - Complex tables MUST have a title and summary to explain structure (4.4, 4.5) **iOS (SwiftUI):** ```swift // GOOD: accessible table with header List { Section(header: Text("Q3 Revenue by Region")) { ForEach(regions) { region in HStack { Text(region.name) Spacer() Text(region.revenue) .accessibilityLabel("\(region.name): \(region.revenue)") } } } } ``` **Android (Compose):** ```kotlin // GOOD: announce table structure to TalkBack Column(modifier = Modifier.semantics { contentDescription = "Revenue table, 3 regions, 2 columns: region and revenue" }) { // Table content } ``` ### 5. Interactive Components (Topic 5) **This is critical for mobile. Always look up: `bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 5`** - Every interactive component MUST be keyboard/switch accessible (5.1) - Every interactive component MUST have a relevant accessible name (5.2) - Accessible names MUST include any visible text (5.3) - Screen reader users must receive context changes (5.4 — Level AA) - Interactive component states (selected, expanded, disabled) MUST be rendered by assistive technologies (5.5) **iOS (SwiftUI):** ```swift // GOOD: button with state Toggle(isOn: $isEnabled) { Text("Notifications") } // SwiftUI handles accessibility state automatically // GOOD: custom component with traits and state Button(action: toggleExpansion) { HStack { Text("Details") Image(systemName: isExpanded ? "chevron.up" : "chevron.down") } } .accessibilityAddTraits(.isButton) .accessibilityValue(isExpanded ? "expanded" : "collapsed") // GOOD: custom slider Slider(value: $volume, in: 0...100) .accessibilityLabel("Volume") .accessibilityValue("\(Int(volume)) percent") ``` **Android (Compose):** ```kotlin // GOOD: expandable section with state Row( modifier = Modifier .clickable { toggleExpansion() } .semantics { role = Role.Button stateDescription = if (isExpanded) "expanded" else "collapsed" } ) { Text("Details") Icon( if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = null // described by parent semantics ) } // GOOD: disabled button announces state Button( onClick = { /* ... */ }, enabled = false // Compose announces "disabled" to TalkBack ) { Text("Submit") } ``` **React Native:** ```jsx // GOOD: interactive component with state Details ``` ### 6. Mandatory Elements (Topic 6) - Every screen MUST have a title announced by assistive technologies (6.1) - Default human language of the app MUST be identifiable by assistive technologies (6.2) **iOS:** ```swift // SwiftUI: screen title NavigationView { ContentView() .navigationTitle("Settings") } // UIKit: screen title override func viewDidLoad() { super.viewDidLoad() title = "Settings" } // App language: set in Info.plist CFBundleDevelopmentRegion // and Localizable.strings ``` **Android:** ```kotlin // Compose: screen title for TalkBack Scaffold( topBar = { TopAppBar(title = { Text("Settings") }) } ) { /* ... */ } // Activity: label in AndroidManifest.xml // // Language: set in AndroidManifest.xml // ``` ### 7. Information Structure (Topic 7) - Content must use semantic headings exposed to assistive technologies (7.1) - Significant elements must use appropriate semantics (lists, headings, etc.) (7.2) **iOS (SwiftUI):** ```swift // GOOD: heading semantics Text("Account Settings") .font(.title) .accessibilityAddTraits(.isHeader) Text("Privacy") .font(.headline) .accessibilityAddTraits(.isHeader) ``` **Android (Compose):** ```kotlin // GOOD: heading semantics Text( text = "Account Settings", style = MaterialTheme.typography.headlineMedium, modifier = Modifier.semantics { heading() } ) ``` **React Native:** ```jsx Account Settings ``` ### 8. Presentation of Information (Topic 8) - Content must remain visible and functional when text is enlarged to 200% via system settings (8.1) - No loss of information or functionality at 200% enlargement (8.2 — Level AA) - On screens ≥ 320px CSS equivalent, content must not require both horizontal and vertical scrolling (8.2) - Landscape AND portrait orientations MUST be supported unless a specific orientation is essential (8.3) - Focus indicator must be visible on all interactive elements (8.4) - Content revealed on hover/focus must be dismissable, hoverable, and persistent (8.5) - Content hidden from screen but exposed to AT must still be accessible (8.6) - Content that appears on focus/hover must not obstruct other content without a dismiss mechanism (8.7 — Level AA) **iOS:** ```swift // GOOD: support Dynamic Type Text("Welcome back") .font(.body) // respects system text size // Never use fixed point sizes for body text // GOOD: allow both orientations in Info.plist // UISupportedInterfaceOrientations: all orientations ``` **Android:** ```kotlin // GOOD: use sp (scalable pixels) for text Text( text = "Welcome back", fontSize = 16.sp // scales with system settings ) // GOOD: support rotation in AndroidManifest.xml // android:screenOrientation="unspecified" ``` ### 9. Forms (Topic 9) **Critical topic for mobile apps. Always look up: `bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 9`** - Every form field MUST have an accessible label (9.1) - Labels must be relevant (9.2) - Accessible name must include visible label text (9.3) - Related fields must be grouped with a group label (9.4) - Same-purpose fields must be identifiable programmatically across the app (9.5) - Required fields must be indicated (9.7) - Required fields' indication must be accessible (9.8) - Error messages must be linked to the field and describe expected format (9.9) - Input suggestions on error must be relevant (9.10 — Level AA) - User must be able to review/modify/confirm data before final submission (9.11 — Level AA) - Autocomplete for personal data fields (9.12 — Level AA) **iOS (SwiftUI):** ```swift // GOOD: labelled text field TextField("Email address", text: $email) .textContentType(.emailAddress) // enables autocomplete (9.12) .keyboardType(.emailAddress) .accessibilityLabel("Email address") .accessibilityHint("Required. Format: name@example.com") // GOOD: field group Section(header: Text("Personal information")) { TextField("First name", text: $firstName) .textContentType(.givenName) TextField("Last name", text: $lastName) .textContentType(.familyName) } // GOOD: error message TextField("Email address", text: $email) .accessibilityLabel("Email address") if let error = emailError { Text(error) .foregroundColor(.red) .accessibilityLabel("Error: \(error)") // Post notification so VoiceOver announces immediately } ``` **Android (Compose):** ```kotlin // GOOD: labelled text field with error OutlinedTextField( value = email, onValueChange = { email = it }, label = { Text("Email address *") }, isError = emailError != null, supportingText = emailError?.let { { Text(it) } }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), modifier = Modifier.semantics { contentDescription = "Email address, required" if (emailError != null) { error("Error: $emailError") } } ) // GOOD: autofill hint (9.12) OutlinedTextField( value = firstName, onValueChange = { firstName = it }, label = { Text("First name") }, modifier = Modifier.autofill( autofillTypes = listOf(AutofillType.PersonFirstName), onFill = { firstName = it } ) ) ``` **React Native:** ```jsx // GOOD: accessible form field Email address * {emailError && ( {emailError} )} ``` ### 10. Navigation (Topic 10) - Every interactive component must be accessible and operable by keyboard and any pointing device (10.1) - The application must not contain any keyboard or focus traps (10.2) - Tab/swipe order must be consistent with visual reading order (10.3) - Keyboard shortcuts using a single key must be controllable (disable, remap, or only active on focus) (10.4) **iOS (SwiftUI):** ```swift // GOOD: logical focus order VStack { TextField("First name", text: $firstName) TextField("Last name", text: $lastName) Button("Submit") { submit() } } .accessibilityElement(children: .contain) // SwiftUI follows visual order by default // BAD: custom accessibility sort that breaks logical order // .accessibilitySortPriority() — use only when needed ``` **Android:** ```kotlin // GOOD: traversal order follows layout Column { TextField(/* first name */) TextField(/* last name */) Button(onClick = { submit() }) { Text("Submit") } } // Compose follows composition order by default // For XML views, use android:accessibilityTraversalBefore/After // sparingly and only to fix non-obvious order issues ``` ### 11. Consultation (Topic 11) - No automatic refresh without user control (11.1) - User must be able to control each time limit (extend, remove, or ≥20h) (11.2) - Moving/blinking content must have a pause/stop mechanism (11.3, 11.4) - No flashing content > 3 flashes per second (11.5) - Abrupt context changes must be triggered by user action only, or user must be able to disable them (11.6, 11.7) - Content viewable regardless of screen orientation (portrait/landscape) unless essential (11.8) - Gestures: complex gestures (multi-pointer, path-based) must have single-pointer alternatives (11.9 — Level AA) - Touch actions must use up-event (release), not down-event (press); or provide undo mechanism (11.10) - Pointer target size must be at least 24×24 CSS px (11.14 — Level AA) - Dragging actions must have a single-pointer alternative (11.15) - Motion-triggered actions (shake, tilt) must have UI alternatives and be disableable (11.16) **iOS:** ```swift // GOOD: single-pointer alternative to pinch-to-zoom // Provide + / - buttons alongside pinch gesture ZStack { MapView() VStack { Spacer() HStack { Button(action: { zoomIn() }) { Image(systemName: "plus.magnifyingglass") } .accessibilityLabel("Zoom in") .frame(minWidth: 44, minHeight: 44) // minimum touch target Button(action: { zoomOut() }) { Image(systemName: "minus.magnifyingglass") } .accessibilityLabel("Zoom out") .frame(minWidth: 44, minHeight: 44) } } } ``` **Minimum touch target sizes:** ```swift // iOS: Apple recommends 44×44 pt minimum .frame(minWidth: 44, minHeight: 44) // RAAM 1.1 criterion 11.14: 24×24 CSS px minimum (AA) // Using 44pt is safer and exceeds the requirement ``` ```kotlin // Android: minimum 48dp (exceeds RAAM's 24px requirement) Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp) ``` ### 12–15. Extended Topics (EN 301 549) These topics cover documentation, editing tools, support services, and real-time communication. Query them individually when relevant: ```bash bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 12 # Documentation & accessibility features bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 13 # Editing tools bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 14 # Support services bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh topic 15 # Real-time communication ``` Key rules for these topics: - Documentation must describe accessibility features and how to use them (12.1 — Level AA) - Accessibility features must not be removed or deactivated (12.3) - Editing tools must support creation of accessible content (13.1–13.6) - Support services must communicate accessibility info (14.1–14.3) - Real-time text communication must meet EN 301 549 requirements (15.1–15.11) --- ## Platform-specific accessibility APIs — Quick reference | Feature | iOS (SwiftUI) | iOS (UIKit) | Android (Compose) | Android (XML) | React Native | Flutter | |---------|---------------|-------------|-------------------|---------------|--------------|---------| | Label | `.accessibilityLabel()` | `.accessibilityLabel` | `Modifier.semantics { contentDescription }` | `contentDescription` | `accessibilityLabel` | `Semantics(label:)` | | Hint | `.accessibilityHint()` | `.accessibilityHint` | `Modifier.semantics { stateDescription }` | — | `accessibilityHint` | `Semantics(hint:)` | | Hidden | `.accessibilityHidden(true)` | `.isAccessibilityElement = false` | `Modifier.semantics { invisibleToUser() }` | `importantForAccessibility="no"` | `accessible={false}` | `ExcludeSemantics()` | | Heading | `.accessibilityAddTraits(.isHeader)` | `.accessibilityTraits = .header` | `Modifier.semantics { heading() }` | `accessibilityHeading` | `accessibilityRole="header"` | `Semantics(header: true)` | | Button | `.accessibilityAddTraits(.isButton)` | `.accessibilityTraits = .button` | `Modifier.semantics { role = Role.Button }` | `role="button"` | `accessibilityRole="button"` | `Semantics(button: true)` | | Live region | `.accessibilityAddTraits(.updatesFrequently)` | `.accessibilityTraits = .updatesFrequently` | `Modifier.semantics { liveRegion = LiveRegionMode.Polite }` | `accessibilityLiveRegion="polite"` | `accessibilityLiveRegion="polite"` | `Semantics(liveRegion:)` | --- ## Pre-commit checklist (apply before finalizing any code) - [ ] All informative graphic elements have accessible alternatives - [ ] All decorative graphic elements are hidden from assistive technologies - [ ] Colour is never the sole means of conveying information - [ ] Text contrast ≥ 4.5:1 (normal) / 3:1 (large) - [ ] All interactive elements are reachable via screen reader swipe navigation - [ ] All interactive elements are operable by keyboard/switch access - [ ] Component states (expanded, selected, disabled) are exposed to AT - [ ] Every screen has a title announced by AT - [ ] App language is declared for AT pronunciation - [ ] Headings use proper accessibility traits/semantics - [ ] All form fields have associated labels - [ ] Required fields are indicated accessibly - [ ] Error messages are linked to their fields and describe expected input - [ ] Autocomplete is set for personal data fields - [ ] Both portrait and landscape orientations work - [ ] Content scales correctly with system font size (200%) - [ ] Complex gestures have single-pointer alternatives - [ ] Touch targets are at least 44×44pt (iOS) / 48×48dp (Android) - [ ] No keyboard/focus traps exist - [ ] No auto-playing media without controls --- ## When in doubt 1. Look up the specific RAAM criterion: `bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh criterion ` 2. Check the test methodology (includes iOS AND Android steps): `bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh methodology ` 3. Consult the glossary: `bash ${CLAUDE_SKILL_DIR}/../scripts/raam-lookup.sh glossary ""` 4. Use native platform components over custom ones — they have built-in accessibility 5. Test with VoiceOver (iOS) and TalkBack (Android) mentally: would every element be announced correctly?