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
- Always detect high contrast — use
MediaQuery.highContrastOf(context) - Provide both light and dark high contrast themes
- Test with real users — automated testing can’t catch everything
- Use semantic colors — don’t rely on color alone for meaning
- Maintain WCAG AA compliance — 4.5:1 for normal text, 3:1 for large text
- 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.