Flutter Dark Mode and Theme Switching

Many users prefer a dark screen at night, and some choose it all day to save battery on OLED displays. Flutter makes dark mode simple. You describe a light theme and a dark theme once, and every widget picks up the right colors automatically. Offering both styles feels modern and respects user choice.

The Three Pieces


  MaterialApp
   ├── theme:      light colors
   ├── darkTheme:  dark colors
   └── themeMode:  which one to use
                     ├── ThemeMode.light
                     ├── ThemeMode.dark
                     └── ThemeMode.system  (follow the phone)

A Basic Setup

The method ColorScheme.fromSeed builds a harmonious set of colors from a single starting color. Pass Brightness.dark for the dark version.


MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  ),
  darkTheme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.indigo,
      brightness: Brightness.dark,
    ),
  ),
  themeMode: ThemeMode.system,
  home: HomeScreen(),
)

With ThemeMode.system, the app follows the phone setting. When the user flips the phone to dark mode, the app flips with it.

Reading Theme Colors in Widgets

Never hard-code colors such as Colors.white for backgrounds or Colors.black for text. Those values stay the same in both themes and produce unreadable screens. Ask the theme instead.


Container(
  color: Theme.of(context).colorScheme.surface,
  child: Text(
    'Hello',
    style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
  ),
)
ColorScheme NameUse For
primaryMain brand color, key buttons
onPrimaryText and icons on a primary color
surfaceCard and sheet backgrounds
onSurfaceText on surfaces
errorError messages and warnings

The "on" colors always contrast with their partner. Pairing them guarantees readable text in both modes.

Letting Users Switch Manually

A toggle needs shared state that rebuilds MaterialApp. A ValueNotifier handles this task without extra packages.


final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: themeNotifier,
      builder: (context, mode, child) {
        return MaterialApp(
          theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo)),
          darkTheme: ThemeData(
            useMaterial3: true,
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark),
          ),
          themeMode: mode,
          home: SettingsScreen(),
        );
      },
    );
  }
}

A switch on the settings screen changes the notifier:


Switch(
  value: themeNotifier.value == ThemeMode.dark,
  onChanged: (isOn) {
    themeNotifier.value = isOn ? ThemeMode.dark : ThemeMode.light;
  },
)

  Switch toggled
       │
       ▼
  themeNotifier.value changes
       │
       ▼
  ValueListenableBuilder rebuilds MaterialApp
       │
       ▼
  Every widget redraws with the new colors

Remembering the Choice

The setting resets each launch unless you store it. Save the chosen mode with shared_preferences and read it before runApp. Store the choice as text, such as "light", "dark", or "system".

Detecting the Current Brightness


final isDark = Theme.of(context).brightness == Brightness.dark;

Use this check when an image or icon needs a different version in each mode, such as a dark logo on a light background.

Design Rules for Dark Themes

  • Avoid pure black for large surfaces. A very dark gray looks softer and reduces eye strain.
  • Reduce color saturation. Bright colors vibrate against dark backgrounds.
  • Keep text contrast high enough to read comfortably, with a ratio of at least 4.5 to 1 for body text.
  • Lighten elevated surfaces instead of using heavy shadows, which disappear on dark backgrounds.

Practice Ideas

Create a settings page with a three-option selector for Light, Dark, and System. Build a card, a button, and a list tile, then flip between modes and check every screen. Save the user's choice so it survives an app restart.

Why Offer Both Themes

Dark mode serves practical needs. People who read in bed prefer low glare. People with light sensitivity find bright screens painful. On OLED phones, dark pixels draw less power and extend battery life. Offering a theme choice turns these needs into a small act of respect for your users.

A central theme saves work. Change one seed color, and buttons, cards, switches, and app bars all update together. Teams that scatter color values across dozens of files spend days on a brand refresh that a theme handles in minutes.

Testing Both Themes

Run every screen in light and dark mode before release. Flip the system setting on a real phone, or change themeMode in code during development. Look for text that disappears, icons that vanish, and images with white backgrounds that glare against a dark surface. Ten minutes of checking per screen prevents most complaints.