Question #86EasyFlutter Basics

What is Flutter accessibility ?

#flutter

Answer

Flutter Accessibility

Flutter Accessibility refers to the practice of making Flutter applications usable by everyone, including people with disabilities such as visual impairments, hearing loss, motor difficulties, or cognitive disabilities. Flutter provides comprehensive built-in support for accessibility features.

Why Accessibility Matters

BenefitDescription
Larger AudienceReach users with disabilities (15% of global population)
Legal ComplianceMeet ADA, WCAG, and other legal requirements
Better UXImproved usability benefits all users
App Store ApprovalRequired for some app stores and markets
SEO BenefitsBetter structure improves discoverability

Accessibility Technologies

Screen Readers

Flutter supports major screen readers:

  • iOS: VoiceOver
  • Android: TalkBack
  • Web: JAWS, NVDA, VoiceOver

Making Widgets Accessible

1. Semantic Labels

Add descriptive labels to widgets for screen readers:

dart
class AccessibleImage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Semantics(
      label: 'Profile picture of John Doe',
      child: Image.network('https://example.com/profile.jpg'),
    );
  }
}

class AccessibleButton extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Semantics(
      label: 'Submit form',
      hint: 'Double tap to submit the form',
      button: true,
      enabled: true,
      child: ElevatedButton(
        onPressed: () {},
        child: Icon(Icons.send),
      ),
    );
  }
}

2. Semantic Properties

dart
class SemanticExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Header
        Semantics(
          header: true,
          child: Text(
            'Welcome',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
        ),

        // Button
        Semantics(
          button: true,
          label: 'Add to cart',
          onTap: () {
            print('Added to cart');
          },
          child: Container(
            padding: EdgeInsets.all(16),
            color: Colors.blue,
            child: Icon(Icons.shopping_cart),
          ),
        ),

        // Link
        Semantics(
          link: true,
          label: 'Read more about our products',
          child: GestureDetector(
            onTap: () {},
            child: Text(
              'Learn more',
              style: TextStyle(
                color: Colors.blue,
                decoration: TextDecoration.underline,
              ),
            ),
          ),
        ),

        // Image with label
        Semantics(
          image: true,
          label: 'Company logo',
          child: Image.asset('assets/logo.png'),
        ),
      ],
    );
  }
}

3. Excluding Decorative Elements

dart
class DecorativeExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Row(
      children: [
        // Decorative icon - hidden from screen readers
        ExcludeSemantics(
          child: Icon(Icons.star, color: Colors.grey),
        ),

        // Important content - accessible
        Text('Featured Product'),

        // Purely decorative divider - excluded
        ExcludeSemantics(
          child: Container(
            width: 2,
            height: 20,
            color: Colors.grey,
          ),
        ),
      ],
    );
  }
}

4. Grouping Related Content

dart
class GroupedSemantics extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MergeSemantics(
      child: Row(
        children: [
          Text('Price: '),
          Text('\$99.99', style: TextStyle(fontWeight: FontWeight.bold)),
        ],
      ),
    );
    // Screen reader announces: "Price: $99.99" as a single unit
  }
}

Text Accessibility

1. Text Scaling

Support dynamic text sizing:

dart
class ScalableText extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Text(
      'This text respects user font size preferences',
      style: TextStyle(fontSize: 16), // Will scale based on system settings
    );
  }
}

// Check current text scale factor
class TextScaleExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final textScaleFactor = MediaQuery.of(context).textScaleFactor;

    return Text(
      'Current scale: ${textScaleFactor.toStringAsFixed(2)}x',
    );
  }
}

2. Text Contrast

Ensure sufficient color contrast (WCAG AA: 4.5:1 for normal text, 3:1 for large text):

dart
class ContrastExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ✅ Good contrast
        Container(
          color: Colors.black,
          padding: EdgeInsets.all(16),
          child: Text(
            'High contrast text',
            style: TextStyle(color: Colors.white),
          ),
        ),

        // ❌ Poor contrast
        Container(
          color: Colors.grey[300],
          padding: EdgeInsets.all(16),
          child: Text(
            'Low contrast text',
            style: TextStyle(color: Colors.grey[400]), // Bad!
          ),
        ),
      ],
    );
  }
}

Interactive Element Accessibility

1. Touch Target Size

Minimum touch target: 48x48 logical pixels:

dart
class TouchTargetExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ❌ Too small
        IconButton(
          iconSize: 16, // Too small!
          onPressed: () {},
          icon: Icon(Icons.close),
        ),

        // ✅ Adequate size
        IconButton(
          iconSize: 24,
          padding: EdgeInsets.all(12), // Ensures 48x48 touch target
          onPressed: () {},
          icon: Icon(Icons.close),
        ),

        // ✅ Custom widget with proper sizing
        GestureDetector(
          onTap: () {},
          child: Container(
            width: 48,
            height: 48,
            alignment: Alignment.center,
            child: Icon(Icons.favorite, size: 24),
          ),
        ),
      ],
    );
  }
}

2. Form Field Labels

dart
class AccessibleForm extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ✅ Good: Clear label
        TextField(
          decoration: InputDecoration(
            labelText: 'Email address',
            hintText: 'Enter your email',
            helperText: 'We will never share your email',
          ),
        ),

        // ✅ Good: Semantic label for custom field
        Semantics(
          label: 'Password',
          hint: 'Enter your password',
          textField: true,
          obscured: true,
          child: TextField(
            obscureText: true,
            decoration: InputDecoration(
              labelText: 'Password',
            ),
          ),
        ),
      ],
    );
  }
}

3. Error Messages

dart
class AccessibleErrorHandling extends StatefulWidget {
  
  _AccessibleErrorHandlingState createState() => _AccessibleErrorHandlingState();
}

class _AccessibleErrorHandlingState extends State<AccessibleErrorHandling> {
  final _formKey = GlobalKey<FormState>();
  String? _errorMessage;

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(
              labelText: 'Email',
              errorText: _errorMessage, // Automatically announced by screen readers
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Email is required';
              }
              if (!value.contains('@')) {
                return 'Please enter a valid email';
              }
              return null;
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // Success
              } else {
                // Error message automatically announced
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('Please fix the errors'),
                    behavior: SnackBarBehavior.floating,
                  ),
                );
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

Navigation and Focus

1. Focus Management

dart
class FocusExample extends StatefulWidget {
  
  _FocusExampleState createState() => _FocusExampleState();
}

class _FocusExampleState extends State<FocusExample> {
  final FocusNode _emailFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();

  
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          focusNode: _emailFocus,
          decoration: InputDecoration(labelText: 'Email'),
          onSubmitted: (_) {
            // Move focus to next field
            FocusScope.of(context).requestFocus(_passwordFocus);
          },
        ),
        TextField(
          focusNode: _passwordFocus,
          decoration: InputDecoration(labelText: 'Password'),
          obscureText: true,
          onSubmitted: (_) {
            // Submit form
            _submitForm();
          },
        ),
      ],
    );
  }

  void _submitForm() {
    print('Form submitted');
  }
}

2. Announcements

dart
class AnnouncementExample extends StatelessWidget {
  void _showAnnouncement(BuildContext context, String message) {
    // Announce message to screen readers
    SemanticsService.announce(
      message,
      TextDirection.ltr,
    );
  }

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        _showAnnouncement(context, 'Item added to cart successfully');
      },
      child: Text('Add to Cart'),
    );
  }
}

Testing Accessibility

1. Manual Testing

bash
# Enable TalkBack (Android)
# Settings > Accessibility > TalkBack

# Enable VoiceOver (iOS)
# Settings > Accessibility > VoiceOver

2. Automated Testing

dart
// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Widget has semantic labels', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Semantics(
            label: 'Profile picture',
            child: Image.network('https://example.com/image.jpg'),
          ),
        ),
      ),
    );

    // Verify semantic label exists
    final semantics = tester.getSemantics(find.byType(Image));
    expect(semantics.label, 'Profile picture');
  });

  testWidgets('Touch targets are large enough', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: IconButton(
          icon: Icon(Icons.close),
          onPressed: () {},
        ),
      ),
    );

    // Check minimum touch target size
    final size = tester.getSize(find.byType(IconButton));
    expect(size.width, greaterThanOrEqualTo(48.0));
    expect(size.height, greaterThanOrEqualTo(48.0));
  });
}

Accessibility Checklist

  • All images have semantic labels
  • All interactive elements have minimum 48x48 touch targets
  • Text has sufficient contrast (4.5:1 minimum)
  • Text scales with system font size
  • Form fields have clear labels
  • Error messages are announced
  • Navigation order is logical
  • Decorative elements are excluded from semantics
  • Focus management is implemented
  • Screen reader testing completed

Tools and Resources

  • Accessibility Scanner (Android): Analyze app accessibility
  • Accessibility Inspector (iOS): Test VoiceOver
  • Flutter DevTools: Inspect semantic tree
  • Color Contrast Analyzer: Check color contrast ratios
yaml
# pubspec.yaml - helpful accessibility packages
dependencies:
  flutter_accessibility_service: ^0.2.0
  accessibility_tools: ^1.0.0

Best Practices

  1. Design for Accessibility First: Don't add it as an afterthought
  2. Test with Real Users: Include users with disabilities in testing
  3. Use Semantic Widgets: Prefer widgets with built-in semantics
  4. Provide Multiple Cues: Don't rely on color alone
  5. Support System Preferences: Respect font size, reduce motion, etc.
  6. Document Accessibility Features: Help users discover accessibility features

Important: Accessibility is not optional—it's essential for creating inclusive applications that everyone can use. Flutter makes it easy to build accessible apps from the start.

Documentation: Flutter Accessibility