Flutter Dark Mode Toggle: Light/Dark Theme for Beginners (2026)

Dark mode is one of the first features users ask for in any app. Flutter makes it surprisingly easy โ€” the entire theming system comes down to three properties on MaterialApp: theme, darkTheme, and themeMode. Once you understand how those three work together, adding a toggle that flips your whole app between light and dark takes only a few lines of code.

This guide covers everything from the zero-code system theme approach to a fully persistent user toggle with SharedPreferences, custom colour schemes, an animated icon button variant, and a dedicated settings screen. Every common beginner mistake has a working fix.

๐Ÿ“‹ Prerequisites

1. How Flutter’s Theming System Works

Flutter’s MaterialApp has three theme-related properties that work together as a system. You need to understand all three before writing any toggle code:

Property What it defines When it’s used
themeYour light ThemeDataWhen themeMode is .light, or .system and OS is in light mode
darkThemeYour dark ThemeDataWhen themeMode is .dark, or .system and OS is in dark mode
themeModeWhich theme to apply right nowAlways โ€” this is the selector that controls the other two
ThemeMode value What it does Best for
ThemeMode.systemFollows the device OS light/dark setting automaticallyApps that want zero effort โ€” just respect OS preference
ThemeMode.lightForces light mode everywhere, ignores OS settingApps locked to light mode, or after user explicitly picks light
ThemeMode.darkForces dark mode everywhere, ignores OS settingApps locked to dark mode, or after user explicitly picks dark
๐Ÿšจ Critical: If you don’t define darkTheme, dark mode does nothing
If you only define theme and not darkTheme, Flutter has no dark theme to switch to. Setting themeMode: ThemeMode.dark will silently fall back to the light theme. Always define both.

2. Step 1 โ€” Follow the System Theme (Zero Code Toggle)

The easiest possible dark mode requires no toggle at all โ€” just let the device decide. Set themeMode: ThemeMode.system and define both themes. Flutter handles the rest automatically when the user changes their OS setting:

๐Ÿ’ก Why brightness: Brightness.dark matters
ColorScheme.fromSeed generates a full Material 3 colour palette from a seed colour. Without brightness: Brightness.dark, it generates a light palette even inside darkTheme. The app technically switches themes but uses wrong colours โ€” backgrounds stay near-white, text becomes invisible. Always include brightness: Brightness.dark in your dark ThemeData.

3. Step 2 โ€” Manual Toggle with a Switch Widget

For a user-controlled toggle, keep themeMode as a stateful variable at the MyApp level, update it when the Switch flips, and pass the callback down to whatever screen holds the toggle. The state must live at the same level as MaterialApp โ€” this is the most common mistake beginners make:

4. Step 3 โ€” Persist the Preference with SharedPreferences

Without persistence, the theme resets to the default every time the app restarts. Add the shared_preferences package to save the user’s choice so it survives app restarts:

Now update _MyAppState to load the saved preference on startup and save it on every toggle:

๐Ÿ’ก Tip: Why load in initState and not build?
Reading from SharedPreferences is async. If you call it inside build(), it runs on every rebuild and causes the Switch to visually snap back before the async read resolves. Loading once in initState() and storing the result as a plain bool in state avoids this entirely โ€” the UI only updates once when the preference is loaded.

5. Step 4 โ€” Custom Colour Schemes for Light and Dark

Using ColorScheme.fromSeed is the easiest approach, but sometimes you need to define specific colours for your brand. Here is how to build a fully custom colour scheme for both themes, with different primary and background colours in each:

Separating your themes into an AppTheme class keeps main.dart clean and makes it easy to adjust colours in one place without hunting through the widget tree.

6. Bonus: Animated Icon Button Toggle

The Switch widget works well, but many apps use a single icon button that animates between a sun and moon. Here is a clean implementation using AnimatedSwitcher:

7. Bonus: Settings Screen with Theme Toggle

In a real multi-screen app, the theme toggle typically lives in a Settings screen โ€” not the AppBar. Here is the full pattern: a BottomNavigationBar app where the Settings tab controls the theme for the whole app:

๐Ÿ’ก Tip: RadioListTile gives users all three options
Offering “Follow system / Light / Dark” as three radio buttons is a better UX pattern than a binary switch because it lets users who prefer the system default keep that without being forced into a manual choice. Many production apps (Gmail, YouTube, Twitter/X) use this exact three-way pattern in their settings.

8. How to Read the Current Theme Anywhere

Once your theme is set up, you often need to read whether the app is in dark or light mode to conditionally change colours, icons, or images. There are two reliable ways to do this:

๐Ÿ’ก Best practice: Prefer ColorScheme over hardcoded colours
Rather than checking isDark and hardcoding colours, use Theme.of(context).colorScheme roles like surface, onSurface, primary, onPrimary, background, and onBackground. These automatically return the right colour for the current theme โ€” no conditional logic needed, and your app looks correct in both modes automatically.

9. Common Beginner Mistakes

Mistake 1: Defining only theme but not darkTheme

Mistake 2: Forgetting brightness: Brightness.dark in the dark theme

Mistake 3: Keeping themeMode state too low in the widget tree

Mistake 4: Switch snapping back โ€” reading SharedPreferences inside build()

Mistake 5: Hardcoding colours instead of using ColorScheme roles

10. Interview Q&A

Q: What is themeMode in Flutter?

A: It is a property on MaterialApp that tells Flutter which theme to apply: ThemeMode.light forces light mode, ThemeMode.dark forces dark mode, and ThemeMode.system follows the device OS setting. It acts as a selector between the theme and darkTheme you define.
Q: What are the three ThemeMode values?

A: ThemeMode.system follows the OS setting automatically. ThemeMode.light forces the light theme defined in theme:. ThemeMode.dark forces the dark theme defined in darkTheme:. If darkTheme is not defined, ThemeMode.dark falls back silently to the light theme.
Q: How do you toggle dark mode with a button or switch?

A: Keep themeMode as a state variable in the StatefulWidget that owns MaterialApp โ€” typically _MyAppState. Pass a callback down to the widget containing the toggle. When the user flips it, call setState() to update themeMode. Because the state lives at the MaterialApp level, every widget in the app re-renders with the new theme.
Q: How do you persist the dark mode preference?

A: Use the shared_preferences package. On each toggle, save a bool with prefs.setBool('isDark', isDark). In initState(), load it with prefs.getBool('isDark') and call setState() to apply it before the first frame renders. This ensures the app opens in the correct theme every restart.
Q: What is ColorScheme.fromSeed and why does dark mode need brightness: Brightness.dark?

A: ColorScheme.fromSeed generates a complete Material 3 colour palette from a single seed colour. Without brightness: Brightness.dark, it generates a light palette even when used inside darkTheme. The app technically switches theme objects but the colours remain light โ€” backgrounds stay near-white and text may become invisible. brightness: Brightness.dark tells the generator to produce dark variants of every colour role.
Q: How do you read the current theme inside a widget?

A: Use Theme.of(context).brightness == Brightness.dark. This responds to your app’s themeMode state rather than always reading the OS setting. For widgets that should adapt to both themes automatically without manual checks, use Theme.of(context).colorScheme roles like surface, onSurface, primary โ€” these return the right colour for the current theme without any conditional logic.
Post Why it’s relevant
Flutter Widgets Explained: Stateless vs StatefulThe dark mode toggle depends entirely on StatefulWidget and setState() โ€” the prerequisite concept to understand first.
Understanding pubspec.yaml: A Beginner’s Complete GuideThe persistence step requires adding shared_preferences as a dependency โ€” this guide explains exactly how to do that.
Flutter Bottom Navigation Bar: Complete Beginner’s GuideThe Settings screen pattern in this guide builds directly on the bottom navigation shell shown there.
Flutter Tutorial for Beginners: From Install to First AppIntroduces MaterialApp, Scaffold, and ThemeData โ€” the three building blocks this guide builds on top of.
Hot Reload vs Hot Restart in FlutterAfter adding shared_preferences to pubspec.yaml, a full restart is required โ€” this guide explains why and when.
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply