Dark and light appearance in mobile apps

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.

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.

Flutter Material Components light appearance
Light appearance
Flutter Material Components dark appearance
Dark appearance

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.light and ThemeMode.dark overrides 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.fromSeed is 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.