Build and publish a Flutter-based app for the App Store and Google Play — revisited and updated for 2026, where dark and light appearance support is no longer optional: it is a baseline expectation for every app on every platform.
TL:DR – When this article was first written, dark mode felt like a novelty feature. That is no longer the case. Dark appearance support is no longer a nice-to-have. It is a baseline expectation on both platforms, and the tooling in Flutter has matured significantly to make it straightforward. If you are starting fresh, use Material 3 and ColorScheme.fromSeed. If you are maintaining an existing Material 2 app, plan your migration to the new type scale and colour roles. Either way, address appearance early in your design process: retrofitting it into a large codebase is genuinely painful.
Contents
- No longer a novelty
- Dark and light appearance: the current landscape
- System settings behaviour
- Material 3 and the updated colour system
- Custom colour theme extended for dark appearance
- Applying the theme in Flutter: Material 2 vs Material 3
- The Material 3 approach: ColorScheme and useMaterial3
- Dark and light text theme in practice
- Make the app show you the theme
No longer a novelty
Both Apple and Google have hardened their design guidelines around adaptive appearance, and users routinely switch between light and dark based on time of day, ambient light sensors, or personal preference. App Store and Google Play review processes increasingly flag apps that look broken or jarring in dark appearance. If you are starting a new Flutter app today, adaptive theming is not something you bolt on later — it is something you design from the first commit.
On the Flutter side, the Material 3 design system (also called Material You) is now the default in Flutter 3.x and beyond. Material 2 is in maintenance mode. This has significant consequences for how themes are defined: the old headline6, bodyText1, and similar text style names are deprecated in favour of a cleaner, more explicit type scale. The colour system has also expanded well beyond the original twelve-colour baseline. If you are working from an older codebase, this article will flag where the old approach still works and where you need to modernise.
Dark and light appearance: the current landscape
All mainstream iOS and Android devices now ship with full dark appearance support. Android 10 and iOS 13 introduced system-level dark mode years ago, and at this point any device running a current OS — or even a moderately recent one — supports it. Supporting light-only is a legacy concern, not a design choice. The question is no longer whether to support both appearances, but how well you support them.
Dark appearance has proven genuinely popular with users. Better usability in low-light environments is the primary driver, and OLED displays — now standard across the mid-to-high range of both Android and iPhone — do benefit from darker interfaces in terms of power draw. The practical upshot: your app will be used in dark mode by a substantial portion of your audience, and it needs to look considered and intentional, not like an afterthought.
Throughout this article, appearance refers to the system-level setting (light or dark), while theme refers to the colour and style configuration inside the Flutter app itself.
System settings behaviour
The simplest and most user-friendly approach remains the same: respect the system setting and do not add a separate in-app toggle unless your use case specifically demands one. Most well-regarded apps — including Apple's own first-party apps and Google's core apps — follow the system setting by default and offer an override only as an accessibility or convenience option buried in settings. For a new app, following the system setting is less work and aligns with user expectations on both platforms.
In Flutter, this is handled by providing both a theme and a darkTheme to your MaterialApp, and setting themeMode: ThemeMode.system. Flutter then responds automatically when the user changes the device appearance setting.
Material 3 and the updated colour system
The original Material Design colour system defined twelve named roles. Material 3 expands this significantly, introducing a more nuanced set of colour roles including primary, onPrimary, primaryContainer, onPrimaryContainer, secondary, tertiary, and their dark-mode counterparts, among others. The concept of Secondary Variant from Material 2 is gone — it was quietly dropped and has no direct equivalent in Material 3.
The practical benefit of the expanded system is that it is designed from the ground up to produce accessible, harmonious colour pairs for both light and dark appearances. Google's Material Theme Builder generates a complete ColorScheme for both light and dark from a single seed colour, which is the recommended starting point for new projects in 2026.
If you are maintaining a Material 2 codebase, the baseline colour table below remains useful as a reference. The left column is the light theme; the right is the dark theme baseline.
| Primary 500 #6200EE |
Primary 200 #BB86FC |
| Primary Variant 700 #3700B3 |
Primary Variant 700 #3700B3 |
| Secondary 200 #03DAC6 |
Secondary 200 #03DAC6 |
| Secondary Variant #018786 |
Removed in M3 |
| Background #FFFFFF |
Background #121212 |
| Surface #FFFFFF |
Surface #121212 |
| Error #B00020 |
Error #CF6679 |
| On Primary #FFFFFF |
On Primary #000000 |
| On Secondary #000000 |
On Secondary #000000 |
| On Background #000000 |
On Background #FFFFFF |
| On Surface #000000 |
On Surface #FFFFFF |
| On Error #FFFFFF |
On Error #000000 |
The "On" colours are applied on top of surfaces that use a primary, secondary, surface, background, or error colour. In dark themes, "On" colours default to white or black depending on contrast requirements. When building a custom theme, these are usually left at their defaults unless contrast ratios demand otherwise.
Custom colour theme extended for dark appearance
Placing both light and dark palettes side by side is still the best way to catch clashes before they reach a device. The table below shows the custom warm-tone palette from this project extended across both appearances.
| Primary 500 #892807 |
Primary 200 #EC7C55 |
| Primary Variant 700 #570000 |
Primary Variant 700 #E76944 |
| Secondary 200 #EC7C55 |
Secondary 200 #F6BEAA |
| Secondary Variant #BF5531 |
Removed in M3 |
| Background #FFFFFF |
Background #121212 |
| Surface #FFFFFF |
Surface #121212 |
| Error #C5032B |
Error #CF6679 |
| On Primary #FFFFFF |
On Primary #000000 |
| On Secondary #000000 |
On Secondary #000000 |
| On Background #000000 |
On Background #FFFFFF |
| On Surface #000000 |
On Surface #FFFFFF |
| On Error #FFFFFF |
On Error #000000 |
Applying the theme in Flutter: Material 2 vs Material 3
The code below reflects the approach used in the original version of this app, which targets Material 2. It is still valid for projects that have not yet migrated, but note that the text style names (headline6, bodyText1, etc.) are deprecated as of Flutter 3.x. The Material 3 equivalents use a new type scale: titleLarge replaces headline6, bodyMedium replaces bodyText2, and so on. The full mapping is documented in the Flutter Material 3 migration guide.
Edit main.dart. Swatches are groups of colours in Material shades; individually named colours need to be referenced explicitly even when they are part of a swatch. Use copyWith on Theme.of(context) to inherit everything from the default theme and override only what your app needs.
// Material 2 approach — valid but deprecated text style names
theme: ThemeData(
primarySwatch: lightwhichisdarker,
primaryColor: lightwhichisdarker[500],
scaffoldBackgroundColor: parchmentLightBackground,
textTheme: Theme.of(context).textTheme.copyWith(
// M2 names shown here; migrate to M3 equivalents for new projects
// headline6 → titleLarge
// bodyText1 → bodyLarge
// bodyText2 → bodyMedium
// subtitle1 → titleMedium
// subtitle2 → titleSmall
// caption → bodySmall
// overline → labelSmall
headline6: TextStyle(color: parchmentLightOnSurface),
subtitle1: TextStyle(color: parchmentLightOnSurface),
bodyText1: TextStyle(color: parchmentLightOnSurface),
bodyText2: TextStyle(color: parchmentLightOnSurface),
caption: TextStyle(color: parchmentLightOnSurface),
),
),
darkTheme: ThemeData(
primarySwatch: darkwhichislighter,
primaryColor: darkwhichislighter[500],
scaffoldBackgroundColor: parchmentDarkBackground,
textTheme: Theme.of(context).textTheme.copyWith(
headline6: TextStyle(color: parchmentDarkOnSurface),
subtitle1: TextStyle(color: parchmentDarkOnSurface),
bodyText1: TextStyle(color: parchmentDarkOnSurface),
bodyText2: TextStyle(color: parchmentDarkOnSurface),
caption: TextStyle(color: parchmentDarkOnSurface),
),
),
themeMode: ThemeMode.system,
The Material 3 approach: ColorScheme and useMaterial3
For new Flutter projects in 2026, the recommended pattern uses ColorScheme.fromSeed and sets useMaterial3: true. This generates a full, accessible colour scheme from a single seed colour, including all dark-mode counterparts. You no longer need to define every colour role manually.
// Material 3 approach — recommended for new projects
MaterialApp(
themeMode: ThemeMode.system,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Color(0xFF892807),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Color(0xFF892807),
brightness: Brightness.dark,
),
),
);
Text styles in Material 3 are referenced via Theme.of(context).textTheme.titleLarge, .bodyMedium, and so on. The type scale is cleaner and more descriptive than the old numbered headline system.
Dark and light text theme in practice
The screenshots below show the app rendering boilerplate text in both appearances. The text style previously referenced as headline6 — now titleLarge in Material 3 — uses parchmentLightOnSurface in the light theme and parchmentDarkOnSurface in the dark theme, switching automatically when the system appearance changes. No conditional logic, no platform channels — the theme system handles it entirely.


Make the app show you the theme
A dedicated theme-preview screen remains one of the most useful development tools you can build. Drop in all your common components — buttons, cards, text styles, input fields — and flip the device between light and dark to catch contrast problems before they reach a reviewer. A few things worth noting for 2026:
- Remove all test screens and lorem ipsum content before submitting. Apple's App Store review guidelines explicitly flag placeholder text as a rejection reason, and this has not changed.
- Consider using Flutter's
ThemeMode.lightandThemeMode.darkoverrides during development to force a specific appearance without changing the device system setting — faster than toggling system settings repeatedly. - The Flutter DevTools theme inspector can show you your active colour scheme at runtime, which is helpful for verifying that
ColorScheme.fromSeedis generating the roles you expect.
body: Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Sample text for theme preview — remove before release.',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
Always test your app in both light and dark appearances on real hardware, not just the simulator. Colours that look fine on a calibrated desktop monitor can exhibit poor contrast or unexpected tinting on OLED panels at lower brightness levels.