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.