Accessibility: VoiceOver and TalkBack Support

Whistl is designed for everyone, including users with disabilities. This comprehensive guide explains VoiceOver and TalkBack implementation, dynamic type support, colour contrast, switch control, and how Whistl meets WCAG 2.1 AA accessibility standards.

Why Accessibility Matters

Financial protection should be available to all users:

  • Visual impairments: 2.2 billion people have vision impairment (WHO)
  • Motor impairments: Difficulty with precise touch gestures
  • Hearing impairments: Need visual alternatives to audio
  • Cognitive differences: Clear, simple interfaces help everyone
  • Legal compliance: Accessibility is required by law in many regions

Whistl is committed to making financial protection accessible to everyone.

VoiceOver (iOS)

VoiceOver reads screen content aloud for blind and low-vision users:

Accessibility Labels

import SwiftUI

struct TransactionRow: View {
    let transaction: Transaction
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(transaction.merchant)
                    .font(.headline)
                Text(transaction.category)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            Text(transaction.amount, format: .currency(code: "AUD"))
                .font(.body)
                .foregroundColor(transaction.amount < 0 ? .red : .green)
        }
        .padding()
        .accessibilityElement(children: .combine)
        .accessibilityLabel(
            "\(transaction.merchant), \(transaction.category), " +
            "\(transaction.amount, format: .currency(code: "AUD"))"
        )
        .accessibilityHint("Double-tap to view transaction details")
    }
}

Accessibility Traits

struct ProtectedBalanceView: View {
    let balance: Double
    let isProtected: Bool
    
    var body: some View {
        VStack {
            Text("Protected Balance")
                .font(.headline)
            
            Text(balance, format: .currency(code: "AUD"))
                .font(.largeTitle)
                .fontWeight(.bold)
            
            if isProtected {
                Image(systemName: "shield.fill")
                    .foregroundColor(.green)
            }
        }
        .padding()
        .accessibilityElement(children: .combine)
        .accessibilityLabel("Protected Balance: \(balance, format: .currency(code: "AUD"))")
        .accessibilityHint(isProtected ? "Your balance is protected" : "")
        .accessibilityAddTraits(isProtected ? .updatesFrequently : .none)
    }
}

Custom Actions

struct InterventionCard: View {
    let riskLevel: RiskLevel
    
    var body: some View {
        VStack {
            Text("Risk Level: \(riskLevel.description)")
            Button("Call Sponsor") {
                callSponsor()
            }
            Button("Start Breathing") {
                startBreathingExercise()
            }
        }
        .accessibilityElement(children: .contain)
        .accessibilityActions {
            Action(named: "Call Sponsor") {
                callSponsor()
                return true
            }
            Action(named: "Start Breathing Exercise") {
                startBreathingExercise()
                return true
            }
        }
    }
}

TalkBack (Android)

TalkBack provides similar functionality on Android:

Content Descriptions

import androidx.compose.foundation.clickable
import androidx.compose.ui.semantics.*

@Composable
fun TransactionRow(transaction: Transaction) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onTransactionClick(transaction) }
            .semantics {
                contentDescription = 
                    "${transaction.merchant}, ${transaction.category}, " +
                    "${transaction.amount} dollars"
                onClick { 
                    onTransactionClick(transaction)
                    true
                }
                onClickLabel = "View transaction details"
            }
    ) {
        Column {
            Text(transaction.merchant)
            Text(transaction.category)
        }
        Spacer(Modifier.weight(1f))
        Text(transaction.amount.toString())
    }
}

Custom Actions (Compose)

@Composable
fun GoalCard(goal: Goal) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .semantics {
                contentDescription = 
                    "Goal: ${goal.name}, " +
                    "${goal.currentAmount} of ${goal.targetAmount} saved"
                
                customActions = listOf(
                    CustomAccessibilityAction(
                        label = "Add to goal",
                        action = {
                            showAddToGoalDialog(goal)
                            true
                        }
                    ),
                    CustomAccessibilityAction(
                        label = "Edit goal",
                        action = {
                            navigateToEditGoal(goal)
                            true
                        }
                    )
                )
            }
    ) {
        // Goal card content
    }
}

Dynamic Type Support

Text scales with user's system font size preferences:

iOS Dynamic Type

struct ContentView: View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize
    
    var body: some View {
        VStack {
            Text("Welcome to Whistl")
                .font(.title)
                .fontWeight(.bold)
            
            Text("Your financial protection companion")
                .font(.body)
            
            Button("Get Started") {
                // Action
            }
            .font(dynamicTypeSize >= .accessibility1 ? .headline : .body)
            .padding(dynamicTypeSize >= .accessibility1 ? 20 : 12)
        }
        .padding()
    }
}

Android Scalable Text

@Composable
fun WelcomeScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "Welcome to Whistl",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.padding(bottom = 8.dp)
        )
        
        Text(
            text = "Your financial protection companion",
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.padding(bottom = 24.dp)
        )
        
        Button(
            onClick = { onGetStartedClick() },
            modifier = Modifier
                .fillMaxWidth()
                .heightIn(min = 48.dp)  // Minimum touch target
        ) {
            Text("Get Started")
        }
    }
}

Colour Contrast

Whistl meets WCAG AA contrast requirements (4.5:1 for normal text):

Contrast Ratios

ElementForegroundBackgroundRatio
Body Text#171717#FFFFFF16.1:1
Secondary Text#737373#FFFFFF5.0:1
Links#4F46E5#FFFFFF5.8:1
Buttons#FFFFFF#4F46E55.8:1
Error Messages#DC2626#FFFFFF5.9:1
Success Messages#16A34A#FFFFFF4.6:1

Dark Mode Support

// SwiftUI Dark Mode
struct ContentView: View {
    var body: some View {
        Text("Welcome")
            .foregroundColor(.primary)  // Adapts to light/dark
        Text("Subtitle")
            .foregroundColor(.secondary)  // Adapts to light/dark
        Button("Action") {
            // Action
        }
        .buttonStyle(.borderedProminent)  // Adapts to light/dark
    }
}

// Jetpack Compose Dark Mode
@Composable
fun WelcomeText() {
    Text(
        "Welcome",
        color = MaterialTheme.colorScheme.onBackground
    )
    Text(
        "Subtitle",
        color = MaterialTheme.colorScheme.onSurfaceVariant
    )
}

Touch Target Sizes

All interactive elements meet minimum size requirements:

Minimum Touch Targets

PlatformMinimum SizeRecommended
iOS44x44 points48x48 points
Android48x48 dp56x56 dp
WCAG44x44 CSS pixels

Implementation

// SwiftUI - Minimum touch target
Button("Delete") {
    deleteItem()
}
.frame(minWidth: 44, minHeight: 44)

// Jetpack Compose - Minimum touch target
IconButton(
    onClick = { deleteItem() },
    modifier = Modifier.size(48.dp)
) {
    Icon(Icons.Default.Delete, contentDescription = "Delete")
}

Switch Control

Users with motor impairments can navigate with switches:

Focus Order

struct OnboardingView: View {
    var body: some View {
        VStack {
            Text("Welcome to Whistl")
                .accessibilitySortPriority(1)
            
            Image("onboarding-illustration")
                .accessibilityHidden(true)  // Decorative
            
            Text("Let's set up your protection")
                .accessibilitySortPriority(2)
            
            Button("Continue") {
                // Action
            }
            .accessibilitySortPriority(3)
            
            Button("Skip") {
                // Action
            }
            .accessibilitySortPriority(4)
        }
    }
}

Reduced Motion

Animations respect system reduced motion settings:

Motion Adaptation

struct AnimatedTransition: View {
    @Environment(\.accessibilityReduceMotion) var reduceMotion
    @State private var isVisible = false
    
    var body: some View {
        Content()
            .opacity(isVisible ? 1 : 0)
            .scaleEffect(isVisible ? 1 : 0.9)
            .animation(
                reduceMotion ? .none : .spring(duration: 0.5),
                value: isVisible
            )
            .onAppear {
                isVisible = true
            }
    }
}

Screen Reader Testing

Whistl is tested with actual screen reader users:

Testing Checklist

  • Navigation: Can navigate all screens with VoiceOver/TalkBack
  • Labels: All elements have meaningful labels
  • Hints: Interactive elements have action hints
  • Focus order: Logical reading order
  • Dynamic type: Text scales correctly at all sizes
  • Contrast: All text meets contrast requirements
  • Touch targets: All buttons meet minimum size
  • Forms: Labels associated with inputs
  • Errors: Error messages announced to screen readers
  • Live regions: Dynamic content changes announced

Accessibility Features Summary

FeatureiOSAndroid
Screen ReaderVoiceOverTalkBack
Dynamic TypeDynamic TypeFont Scale
Reduce MotionReduce MotionRemove Animations
Invert ColoursSmart InvertColour Inversion
Switch ControlSwitch ControlSwitch Access
Voice ControlVoice ControlVoice Access
Display AccommodationsDisplay & Text SizeVisibility Enhancements

Conclusion

Whistl is committed to accessibility for all users. Through VoiceOver and TalkBack support, dynamic type, colour contrast, and switch control, financial protection is available to everyone regardless of ability.

Accessibility isn't an afterthought—it's built into Whistl from the ground up.

Accessible Protection for Everyone

Whistl meets WCAG 2.1 AA accessibility standards. Download free and experience inclusive financial protection.

Download Whistl Free

Related: Multi-Language Localization | Crash Reporting | Privacy-Compliant Analytics