← Articles

Dark mode implementation in Flutter

By John · 7 July 2025

Dark mode is now expected on both iOS and Android. Flutter's theming system makes it straightforward to support both, but doing it right — consistent colors, proper image handling, no flash on launch — takes care.

Define your themes

class AppTheme {
  static ThemeData get light => ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF6750A4),
      brightness: Brightness.light,
    ),
    // Override specific tokens
    cardTheme: const CardThemeData(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
    ),
  );

  static ThemeData get dark => ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF6750A4),
      brightness: Brightness.dark,
    ),
    cardTheme: const CardThemeData(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(12)),
      ),
    ),
  );
}

// In MaterialApp:
MaterialApp(
  theme: AppTheme.light,
  darkTheme: AppTheme.dark,
  themeMode: ThemeMode.system, // Follow system setting
)

Let users override the system setting

// Store the user's preference
const _kThemeKey = 'theme_mode';

class ThemeRepository {
  final SharedPreferences _prefs;
  ThemeRepository(this._prefs);

  ThemeMode getThemeMode() {
    final value = _prefs.getString(_kThemeKey);
    return switch (value) {
      'light' => ThemeMode.light,
      'dark' => ThemeMode.dark,
      _ => ThemeMode.system,
    };
  }

  Future<void> setThemeMode(ThemeMode mode) async {
    final value = switch (mode) {
      ThemeMode.light => 'light',
      ThemeMode.dark => 'dark',
      ThemeMode.system => 'system',
    };
    await _prefs.setString(_kThemeKey, value);
  }
}

// Riverpod provider
@riverpod
class ThemeModeNotifier extends _$ThemeModeNotifier {
  @override
  ThemeMode build() {
    return ref.read(themeRepositoryProvider).getThemeMode();
  }

  Future<void> setMode(ThemeMode mode) async {
    await ref.read(themeRepositoryProvider).setThemeMode(mode);
    state = mode;
  }
}

// Wire it up
Consumer(
  builder: (context, ref, child) {
    final themeMode = ref.watch(themeModeNotifierProvider);
    return MaterialApp(
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      themeMode: themeMode,
    );
  },
)

Using colors correctly in widgets

Always use Theme.of(context).colorScheme — never hard-code hex values in widgets:

// WRONG
container color: const Color(0xFFFFFFFF),
text style: TextStyle(color: Colors.black87),

// RIGHT
Container(
  color: Theme.of(context).colorScheme.surface,
  child: Text(
    'Hello',
    style: TextStyle(
      color: Theme.of(context).colorScheme.onSurface,
    ),
  ),
)

Key M3 color roles:

surface / onSurface         — cards, dialogs, backgrounds
primary / onPrimary         — prominent buttons, FABs
primaryContainer / onPrimaryContainer — filled chips, tonal surfaces
error / onError             — error states
outline                     — borders
outlineVariant              — dividers, subtle borders

Custom semantic colors

For app-specific colors (e.g., success, warning, custom brand colors):

extension AppColorScheme on ColorScheme {
  Color get success => brightness == Brightness.light
      ? const Color(0xFF2E7D32)
      : const Color(0xFF81C784);

  Color get onSuccess => brightness == Brightness.light
      ? Colors.white
      : const Color(0xFF1B5E20);

  Color get warning => brightness == Brightness.light
      ? const Color(0xFFF57C00)
      : const Color(0xFFFFB74D);
}

// Usage:
Color successColor = Theme.of(context).colorScheme.success;

Status bar and navigation bar theming

// Set status bar color based on theme
class _MyAppState extends State<MyApp> {
  @override
  void didChangePlatformBrightness() {
    super.didChangePlatformBrightness();
    _updateSystemUI();
  }

  void _updateSystemUI() {
    final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;
    final isDark = brightness == Brightness.dark;
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      statusBarBrightness: isDark ? Brightness.dark : Brightness.light,
      statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
      systemNavigationBarColor: isDark ? const Color(0xFF1C1B1F) : Colors.white,
      systemNavigationBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
    ));
  }
}

Images for dark mode

// Different images per theme
Image.asset(
  isDarkMode ? 'assets/logo_dark.png' : 'assets/logo_light.png',
)

// Or use SVG with currentColor
// SVGs that reference 'currentColor' adapt to text color automatically
SvgPicture.asset(
  'assets/icon.svg',
  colorFilter: ColorFilter.mode(
    Theme.of(context).colorScheme.primary,
    BlendMode.srcIn,
  ),
)

Avoiding a white flash on dark-mode launch

The native splash screen shows before Flutter loads. Configure it to match:

Androidandroid/app/src/main/res/values-night/styles.xml:

<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
  <item name="android:windowBackground">#1C1B1F</item>
</style>

iOS — set UIBackgroundColor in Info.plist or use flutter_native_splash which handles this automatically.

Detecting current mode in widgets

bool isDark = Theme.of(context).brightness == Brightness.dark;
// Or
bool isDark = MediaQuery.of(context).platformBrightness == Brightness.dark;

Prefer the Theme approach (respects user override), not MediaQuery (reflects system only).

Common pitfalls

Hard-coding black or white. Colors.black is invisible on a dark background, Colors.white pops badly in dark mode. Use colorScheme.onSurface instead.

Not testing dark mode during development. Enable dark mode on the simulator/emulator from settings and keep it there while building UI. Issues that are obvious in dark mode are invisible in light mode.

Custom painters ignoring brightness. CustomPainter draws with explicit Paint() colors — these don't adapt automatically. Check brightness in the painter:

@override
void paint(Canvas canvas, Size size) {
  final isDark = Theme.of(context).brightness == Brightness.dark;
  final paint = Paint()..color = isDark ? Colors.white54 : Colors.black54;
  // ...
}

Sign in to like, dislike, or report.

Dark mode implementation in Flutter — ANN Tech