Dart Tutorial

Flutter Lesson 26 of 83 3 min read

Themes and Dark Mode in Flutter

Style a whole Flutter app with ThemeData and ColorScheme: seed colours, text themes, component themes, dark mode and a theme switch.

On this page

A theme defines colours, typography and component styles in one place. Widgets read from it, so the whole app stays consistent and can be restyled by changing a few lines.

A theme from one colour #

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system, // follow the device setting
      home: Scaffold(
        appBar: AppBar(title: const Text('Themed app')),
        body: Center(
          child: FilledButton(onPressed: () {}, child: const Text('Primary action')),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add),
        ),
      ),
    ),
  );
}

ColorScheme.fromSeed generates a complete, accessible palette from one colour, for both light and dark. Change the seed and every component follows.

Using theme colours #

Never hard-code colours in widgets. Read them from the scheme, and they will be correct in dark mode too.

final colors = Theme.of(context).colorScheme;

Container(
  color: colors.primaryContainer,
  child: Text('Hello', style: TextStyle(color: colors.onPrimaryContainer)),
)
RoleUsed for
primary / onPrimaryMain buttons and the content on them
primaryContainer / onPrimaryContainerTinted highlights
secondary, tertiarySupporting accents
surface / onSurfacePage and card backgrounds, and normal text
surfaceContainerHighestSlightly raised surfaces
onSurfaceVariantSecondary text and icons
outline, outlineVariantBorders and dividers
error / onErrorErrors

Every colour has an on... partner that is guaranteed to be readable on top of it.

Text styles #

final text = Theme.of(context).textTheme;

Text('Heading', style: text.headlineSmall)
Text('Body', style: text.bodyLarge)

Component themes #

Style every instance of a component once.

ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  appBarTheme: const AppBarTheme(centerTitle: true),
  cardTheme: CardThemeData(
    elevation: 0,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  ),
  inputDecorationTheme: const InputDecorationTheme(
    border: OutlineInputBorder(),
    filled: true,
  ),
  filledButtonTheme: FilledButtonThemeData(
    style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(48)),
  ),
)

Letting the user choose #

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _mode = ThemeMode.system;

  @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: _mode,
      home: Scaffold(
        appBar: AppBar(title: const Text('Appearance')),
        body: Center(
          child: SegmentedButton<ThemeMode>(
            segments: const [
              ButtonSegment(value: ThemeMode.system, label: Text('System')),
              ButtonSegment(value: ThemeMode.light, label: Text('Light')),
              ButtonSegment(value: ThemeMode.dark, label: Text('Dark')),
            ],
            selected: {_mode},
            onSelectionChanged: (s) => setState(() => _mode = s.first),
          ),
        ),
      ),
    );
  }
}

Save the choice with shared_preferences so it survives a restart.

Checking the current brightness #

final isDark = Theme.of(context).brightness == Brightness.dark;

Use this sparingly, for things like choosing between two image assets. For colours, the scheme already does the work.

Theming part of the screen #

Theme(
  data: Theme.of(context).copyWith(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
  ),
  child: const PromoBanner(),
)

Your own theme values #

For values Material does not have, such as a “success” colour, create a ThemeExtension.

class AppColors extends ThemeExtension<AppColors> {
  const AppColors({required this.success});
  final Color success;

  @override
  AppColors copyWith({Color? success}) => AppColors(success: success ?? this.success);

  @override
  AppColors lerp(AppColors? other, double t) =>
      other == null ? this : AppColors(success: Color.lerp(success, other.success, t)!);
}

// In ThemeData:   extensions: const [AppColors(success: Color(0xFF1E8E3E))]
// In a widget:    Theme.of(context).extension<AppColors>()!.success

Keep the theme in its own file #

// lib/app_theme.dart
abstract final class AppTheme {
  static ThemeData light() => _build(Brightness.light);
  static ThemeData dark() => _build(Brightness.dark);

  static ThemeData _build(Brightness brightness) => ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal, brightness: brightness),
        appBarTheme: const AppBarTheme(centerTitle: true),
      );
}

Try it yourself #

Create a theme with your own seed colour, centred app bar titles, outlined text fields and full-width filled buttons. Add a switch in the app bar that toggles between light and dark.

Practise in the playground Updated by Santosh Adhikari