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 Name | Use For |
|---|---|
| primary | Main brand color, key buttons |
| onPrimary | Text and icons on a primary color |
| surface | Card and sheet backgrounds |
| onSurface | Text on surfaces |
| error | Error 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.
