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)),
)
| Role | Used for |
|---|---|
primary / onPrimary | Main buttons and the content on them |
primaryContainer / onPrimaryContainer | Tinted highlights |
secondary, tertiary | Supporting accents |
surface / onSurface | Page and card backgrounds, and normal text |
surfaceContainerHighest | Slightly raised surfaces |
onSurfaceVariant | Secondary text and icons |
outline, outlineVariant | Borders and dividers |
error / onError | Errors |
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.