Skip to content
Blog

High Contrast Themes in Flutter: Accessibility-First Design

Learn how to implement high contrast themes in Flutter for users with low vision. Support dark mode, high contrast, and WCAG color compliance.

Published on September 19, 2026

AI Assistant

High Contrast Themes in Flutter: Accessibility-First Design

High contrast themes are essential for users with low vision. Flutter provides built-in support for detecting high contrast mode and adapting your app’s color scheme accordingly. Proper implementation ensures your app meets WCAG contrast requirements and provides a comfortable reading experience for all users.

Why High Contrast Matters

Over 2.2 billion people globally have vision impairments. High contrast themes help users with:

  • Low vision — difficulty distinguishing similar colors
  • Color blindness — inability to perceive certain color differences
  • Cataracts — reduced contrast sensitivity
  • Aging eyes — natural decline in contrast perception

Detecting High Contrast in Flutter

Flutter exposes high contrast settings through MediaQuery:

final highContrast = MediaQuery.highContrastOf(context);
final brightness = MediaQuery.platformBrightnessOf(context);

Implementing High Contrast Themes

Basic Theme Adaptation

class AdaptiveTheme extends StatelessWidget {
  final Widget child;

  const AdaptiveTheme({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    final highContrast = MediaQuery.highContrastOf(context);
    final brightness = MediaQuery.platformBrightnessOf(context);

    return MaterialApp(
      theme: _buildTheme(brightness, highContrast),
      home: child,
    );
  }

  ThemeData _buildTheme(Brightness brightness, bool highContrast) {
    if (highContrast) {
      return ThemeData(
        brightness: brightness,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: brightness,
          // High contrast: maximize contrast
          primary: brightness == Brightness.dark
              ? Colors.white
              : Colors.black,
          onPrimary: brightness == Brightness.dark
              ? Colors.black
              : Colors.white,
          surface: brightness == Brightness.dark
              ? Colors.black
              : Colors.white,
          onSurface: brightness == Brightness.dark
              ? Colors.white
              : Colors.black,
        ),
        // High contrast text themes
        textTheme: const TextTheme(
          bodyLarge: TextStyle(fontWeight: FontWeight.w500),
          bodyMedium: TextStyle(fontWeight: FontWeight.w500),
        ),
        // High contrast icon theme
        iconTheme: IconThemeData(
          color: brightness == Brightness.dark
              ? Colors.white
              : Colors.black,
        ),
      );
    }

    return ThemeData(
      brightness: brightness,
      colorScheme: ColorScheme.fromSeed(
        seedColor: Colors.blue,
        brightness: brightness,
      ),
    );
  }
}

WCAG-Compliant Colors

class WCAGColors {
  // WCAG AA: 4.5:1 contrast ratio for normal text
  // WCAG AAA: 7:1 contrast ratio for normal text

  static const Color darkBackground = Color(0xFF121212);
  static const Color darkSurface = Color(0xFF1E1E1E);
  static const Color darkOnSurface = Color(0xFFFFFFFF); // 15.4:1 contrast

  static const Color lightBackground = Color(0xFFFFFFFF);
  static const Color lightSurface = Color(0xFFF5F5F5);
  static const Color lightOnSurface = Color(0xFF000000); // 21:1 contrast

  // Accessible blue (meets WCAG AA on both backgrounds)
  static const Color accessibleBlue = Color(0xFF0066CC);

  // Check contrast ratio
  static double contrastRatio(Color foreground, Color background) {
    final l1 = _relativeLuminance(foreground);
    final l2 = _relativeLuminance(background);
    final lighter = l1 > l2 ? l1 : l2;
    final darker = l1 > l2 ? l2 : l1;
    return (lighter + 0.05) / (darker + 0.05);
  }

  static double _relativeLuminance(Color color) {
    final r = _linearize(color.red / 255);
    final g = _linearize(color.green / 255);
    final b = _linearize(color.blue / 255);
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  }

  static double _linearize(double component) {
    return component <= 0.03928
        ? component / 12.92
        : pow((component + 0.055) / 1.055, 2.4).toDouble();
  }
}

Dark Mode with High Contrast

class DarkHighContrastTheme {
  static ThemeData get theme {
    return ThemeData(
      brightness: Brightness.dark,
      colorScheme: const ColorScheme.dark(
        primary: Colors.white,
        onPrimary: Colors.black,
        secondary: Colors.cyanAccent,
        onSecondary: Colors.black,
        surface: Color(0xFF0A0A0A),
        onSurface: Colors.white,
        error: Colors.redAccent,
        onError: Colors.black,
      ),
      scaffoldBackgroundColor: const Color(0xFF000000),
      cardColor: const Color(0xFF0A0A0A),
      dividerColor: Colors.white54,
      textTheme: const TextTheme(
        displayLarge: TextStyle(color: Colors.white),
        displayMedium: TextStyle(color: Colors.white),
        displaySmall: TextStyle(color: Colors.white),
        headlineLarge: TextStyle(color: Colors.white),
        headlineMedium: TextStyle(color: Colors.white),
        headlineSmall: TextStyle(color: Colors.white),
        titleLarge: TextStyle(color: Colors.white),
        titleMedium: TextStyle(color: Colors.white),
        titleSmall: TextStyle(color: Colors.white),
        bodyLarge: TextStyle(color: Colors.white),
        bodyMedium: TextStyle(color: Colors.white),
        bodySmall: TextStyle(color: Colors.white70),
        labelLarge: TextStyle(color: Colors.white),
        labelMedium: TextStyle(color: Colors.white),
        labelSmall: TextStyle(color: Colors.white70),
      ),
    );
  }
}

Accessible Color Palette

class AccessiblePalette {
  // Colors that work well in both light and dark modes
  static const Map<String, Color> semanticColors = {
    // Success: green that meets WCAG AA
    'success': Color(0xFF2E7D32),
    'successOnLight': Color(0xFF1B5E20),
    'successOnDark': Color(0xFF81C784),

    // Error: red that meets WCAG AA
    'error': Color(0xFFC62828),
    'errorOnLight': Color(0xFFB71C1C),
    'errorOnDark': Color(0xFFEF5350),

    // Warning: amber that meets WCAG AA
    'warning': Color(0xFFF57F17),
    'warningOnLight': Color(0xFFF57F17),
    'warningOnDark': Color(0xFFFFCA28),

    // Info: blue that meets WCAG AA
    'info': Color(0xFF1565C0),
    'infoOnLight': Color(0xFF0D47A1),
    'infoOnDark': Color(0xFF42A5F5),
  };
}

Testing High Contrast

Visual Testing

testWidgets('high contrast theme', (tester) async {
  await tester.pumpWidget(
    MediaQuery(
      data: const MediaQueryData(
        highContrast: true,
        brightness: Brightness.dark,
      ),
      child: MaterialApp(
        theme: DarkHighContrastTheme.theme,
        home: MyWidget(),
      ),
    ),
  );

  await expectLater(
    find.byType(MyWidget),
    matchesGoldenFile('golden/high_contrast_dark.png'),
  );
});

Contrast Ratio Testing

test('colors meet WCAG AA', () {
  expect(
    WCAGColors.contrastRatio(
      WCAGColors.lightOnSurface,
      WCAGColors.lightBackground,
    ),
    greaterThanOrEqualTo(4.5), // WCAG AA requirement
  );

  expect(
    WCAGColors.contrastRatio(
      WCAGColors.darkOnSurface,
      WCAGColors.darkBackground,
    ),
    greaterThanOrEqualTo(4.5),
  );
});

Best Practices

  1. Always detect high contrast — use MediaQuery.highContrastOf(context)
  2. Provide both light and dark high contrast themes
  3. Test with real users — automated testing can’t catch everything
  4. Use semantic colors — don’t rely on color alone for meaning
  5. Maintain WCAG AA compliance — 4.5:1 for normal text, 3:1 for large text
  6. Consider color blindness — use patterns and labels in addition to color

Resources

High contrast themes are not just a nice-to-have — they’re a requirement for building inclusive Flutter apps. By properly implementing high contrast support, you ensure your app is usable by the widest possible audience.