Migration to Flutter Guide
Discover our battle-tested 21-step framework for a smooth and successful migration to Flutter!
Home
Glossary

Dark and light mode in Flutter

What is dark mode and light mode in Flutter?

Dark and light mode in Flutter means defining separate ThemeData configurations for light and dark themes in MaterialApp, then controlling which one is active through themeMode.

Key facts at a glance

  • MaterialApp accepts theme, darkTheme, and themeMode as separate parameters.
  • CupertinoApp does not expose darkTheme or themeMode. It accepts a single CupertinoThemeData through the theme.
  • ThemeMode.system follows the device's current system brightness; ThemeMode.light and ThemeMode.dark force the app to use the light or dark theme respectively.
  • MediaQuery.platformBrightnessOf(context) reads the current system brightness directly, without going through ThemeMode.
  • Theme.of(context).brightness reads the brightness of the app's currently active ThemeData — that is, the resolved result of theme, darkTheme, and themeMode. By contrast, MediaQuery.platformBrightnessOf(context) reads the system brightness.
  • ColorScheme.fromSeed(seedColor: ..., brightness: Brightness.dark) generates a dark color scheme from the same seed color used for the light scheme.
  • ThemeMode.system follows the platform's system theme setting. On platforms that support system dark mode, it reflects the current system theme.
  • Persisting a user's manual choice (rather than always following the system) typically needs a small storage package like shared_preferences.

How do I add dark and light mode to a Flutter app?

Provide both themes and default to following the system setting with ThemeMode.system.

import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}

Using the same seedColor for both themes keeps the generated color schemes visually related, while the brightness parameter generates a light or dark variant of the scheme.

How do I let users toggle dark mode manually in Flutter?

A ValueNotifier<ThemeMode> combined with a ValueListenableBuilder above MaterialApp is enough for a manual toggle without a full state management package.

final themeModeNotifier = ValueNotifier(ThemeMode.system);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: themeModeNotifier,
      builder: (context, mode, _) {
        return MaterialApp(
          theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(
              seedColor: Colors.teal,
              brightness: Brightness.dark,
            ),
          ),
          themeMode: mode,
          home: const HomeScreen(),
        );
      },
    );
  }
}

// Anywhere in the app:
// themeModeNotifier.value = ThemeMode.dark;

Changing themeModeNotifier.value notifies the ValueListenableBuilder, which rebuilds its subtree with the new themeMode. Because MaterialApp is inside that subtree, it rebuilds and applies the corresponding theme across the app.

Best practices

  1. Default to ThemeMode.system so the app matches the device setting unless the user explicitly overrides it.
  2. Persist a manual override with shared_preferences or similar, so the choice survives an app restart.
  3. Generate both themes from the same seed color with ColorScheme.fromSeed, using the appropriate brightness for each theme.
  4. Test every custom component in both themes, not just the default light one, especially anything using hardcoded colors.
  5. Prefer theme-driven colors over checking Theme.of(context).brightness for styling decisions. Use ColorScheme and other theme properties that adapt to the active theme automatically; check Theme.of(context).brightness when you genuinely need to know the active theme's brightness.
  6. Don't add a custom theme transition just to animate a theme toggle. MaterialApp uses AnimatedTheme to animate changes to the active ThemeData, so changing themeMode already produces an animated theme transition.

Common mistakes

  • Hardcoding colors that look fine in light mode but disappear in dark mode. Any custom color should be checked against both themes, not just the default one.
  • Forcing light or dark mode without considering the user's system preference. If the app does not provide a manual override, ThemeMode.system is usually the better default.
  • Using Theme.of(context).brightness to choose colors manually when the theme already provides the appropriate color. Prefer ColorScheme and other theme properties that adapt to the active theme automatically.
  • Testing only the theme toggle instead of testing individual screens and custom components in both themes. Pay particular attention to custom widgets, dialogs, images, icons, and text styles that may contain hardcoded colors.
  • Forgetting to persist a manual theme choice. If users can explicitly choose light or dark mode, store that preference so it survives an app restart; otherwise, the app will fall back to its initial default.

Learn more

Design system Flutter

Building a Design System in a Large Flutter App

Flutter is known for its seamless UI/UX features and robust design elements. It allowed us to deliver a unique customer experience in the “CA24 Mobile” banking app. This article shares some of the learnings and pain points behind implementing it in this large project.

Get Ready for Edge-to-Edge: Designing Flutter Apps

Mastering Edge-To-Edge in Flutter: A Deep Dive Into the System Navigation Bar in Android

Starting with Android 15, edge-to-edge becomes the default - bringing a modern, immersive feel to apps. In this article, our Flutter developer shows how to handle system bars in Flutter across Android versions and prepare your UI for Android 16, where edge-to-edge will be mandatory.