Flutter Theming — Custom Themes, Colors, and Dark Mode
In this tutorial, you will learn about Flutter Theming. We cover key concepts, practical examples, and best practices to help you master this topic.
Flutter theming centralizes visual properties like colors, typography, and component styles into ThemeData objects, enabling consistent branding and automatic dark mode support across the entire app.
What Will You Learn
- Creating a custom theme with ThemeData
- Color schemes and Material Design 3
- Typography with TextTheme
- Dark mode and theme switching
- Theme extensions for custom properties
- Overriding themes for specific widgets
- Adaptive theming for platform differences
Why It Matters
A consistent visual design is critical for professional apps. Flutter's theming system ensures that every widget uses the same colors, fonts, and styles without manual configuration. Theme changes propagate throughout the widget tree automatically. Implementing dark mode with ThemeData requires minimal code and improves user experience. Material Design 3 provides dynamic color generation from wallpapers on Android 12+.
Real-World Use
The DodaTech Flutter app defines a complete theme system with light and dark variants. The theme includes custom color schemes (primary, secondary, tertiary), typography with two font families, component themes for buttons and cards, and a custom AppColors theme extension for brand-specific colors. Users can switch themes from settings, and the change applies instantly.
Learning Path
flowchart LR A[Flutter Networking] --> B[Flutter Theming\nYou are here] B --> C[Flutter Animations] style B fill:#f90,color:#fff
Basic Theme Setup
Define a theme in MaterialApp to apply it to all descendant widgets:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
theme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorSchemeSeed: Colors.blue,
useMaterial3: true,
brightness: Brightness.dark,
),
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Theming')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Styled with theme', style: Theme.of(context).textTheme.headlineMedium),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {},
child: Text('Themed Button'),
),
],
),
),
);
}
}
colorSchemeSeed generates a complete color scheme from a single color. useMaterial3: true enables Material Design 3 components. brightness controls light/dark mode.
Custom Color Scheme
Define a custom color scheme explicitly:
ThemeData customLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF1565C0),
onPrimary: Colors.white,
secondary: Color(0xFF7B1FA2),
onSecondary: Colors.white,
tertiary: Color(0xFF2E7D32),
onTertiary: Colors.white,
error: Color(0xFFD32F2F),
onError: Colors.white,
surface: Color(0xFFF5F5F5),
onSurface: Color(0xFF212121),
outline: Color(0xFFBDBDBD),
),
);
ThemeData customDarkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFF90CAF9),
onPrimary: Color(0xFF0D47A1),
secondary: Color(0xFFCE93D8),
onSecondary: Color(0xFF4A148C),
tertiary: Color(0xFFA5D6A7),
onTertiary: Color(0xFF1B5E20),
error: Color(0xFFEF9A9A),
onError: Color(0xFFB71C1C),
surface: Color(0xFF303030),
onSurface: Color(0xFFE0E0E0),
outline: Color(0xFF616161),
),
);
Define all required ColorScheme slots for full control. The generated scheme from colorSchemeSeed is sufficient for most apps.
Typography
Define custom text styles with TextTheme:
ThemeData(
textTheme: TextTheme(
displayLarge: TextStyle(
fontSize: 57,
fontWeight: FontWeight.w300,
letterSpacing: -0.25,
),
displayMedium: TextStyle(
fontSize: 45,
fontWeight: FontWeight.w400,
letterSpacing: 0,
),
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w600,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w500,
),
titleLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
),
titleMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
),
),
)
Use Theme.of(context).textTheme.headlineMedium to apply text styles. The 2023 Material Design typography scale includes display, headline, title, body, and label sizes.
Component Themes
Style individual components with component themes:
ThemeData(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF1565C0),
foregroundColor: Colors.white,
elevation: 2,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
textStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
cardTheme: CardThemeData(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
),
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: Color(0xFF1565C0),
foregroundColor: Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
)
Component themes apply to all instances of that widget type. Override individual widgets by applying a style directly.
Theme Extensions
Add custom theme properties with ThemeExtension:
class AppColors extends ThemeExtension<AppColors> {
final Color success;
final Color warning;
final Color info;
final Color? surfaceTint;
AppColors({
required this.success,
required this.warning,
required this.info,
this.surfaceTint,
});
@override
AppColors copyWith({
Color? success,
Color? warning,
Color? info,
Color? surfaceTint,
}) {
return AppColors(
success: success ?? this.success,
warning: warning ?? this.warning,
info: info ?? this.info,
surfaceTint: surfaceTint ?? this.surfaceTint,
);
}
@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
return AppColors(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
info: Color.lerp(info, other.info, t)!,
surfaceTint: Color.lerp(surfaceTint, other.surfaceTint, t),
);
}
static const light = AppColors(
success: Color(0xFF4CAF50),
warning: Color(0xFFFFC107),
info: Color(0xFF2196F3),
);
static const dark = AppColors(
success: Color(0xFF81C784),
warning: Color(0xFFFFD54F),
info: Color(0xFF64B5F6),
);
}
// Usage in theme:
// ThemeData(
// extensions: [AppColors.light],
// )
// Usage in widget:
// final appColors = Theme.of(context).extension<AppColors>()!;
// Container(color: appColors.success)
ThemeExtension adds custom properties to the theme system. They are accessible via Theme.of(context).extension<T>() and participate in theme switching and hot reload.
Dynamic Theme Switching
Implement theme switching with state management:
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
void setThemeMode(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
}
bool get isDarkMode => _themeMode == ThemeMode.dark;
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: ThemedApp(),
),
);
}
class ThemedApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return MaterialApp(
themeMode: themeProvider.themeMode,
theme: customLightTheme,
darkTheme: customDarkTheme,
home: SettingsScreen(),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(title: Text('Settings')),
body: ListView(
children: [
RadioListTile<ThemeMode>(
title: Text('Light'),
value: ThemeMode.light,
groupValue: themeProvider.themeMode,
onChanged: (mode) => themeProvider.setThemeMode(mode!),
),
RadioListTile<ThemeMode>(
title: Text('Dark'),
value: ThemeMode.dark,
groupValue: themeProvider.themeMode,
onChanged: (mode) => themeProvider.setThemeMode(mode!),
),
RadioListTile<ThemeMode>(
title: Text('System'),
value: ThemeMode.system,
groupValue: themeProvider.themeMode,
onChanged: (mode) => themeProvider.setThemeMode(mode!),
),
],
),
);
}
}
ThemeMode.system follows the device setting. ThemeMode.light and ThemeMode.dark override it. The theme change applies instantly because MaterialApp rebuilds when themeMode changes.
Adaptive Theming
Adapt themes to platform conventions:
ThemeData platformAdaptiveTheme(BuildContext context) {
final isIOS = Theme.of(context).platform == TargetPlatform.iOS;
return ThemeData(
useMaterial3: isIOS ? false : true,
appBarTheme: AppBarTheme(
elevation: isIOS ? 0 : 2,
scrolledUnderElevation: isIOS ? 0.5 : 3,
),
navigationBarTheme: NavigationBarThemeData(
height: isIOS ? 85 : 65,
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
),
);
}
Use Theme.of(context).platform to detect the current platform. iOS apps may benefit from Cupertino-style transitions and navigation bars.
Material Design 3 Dynamic Color
Use dynamic color from wallpaper on Android 12+:
ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Color(0xFF1565C0),
brightness: Brightness.light,
),
)
// Or use dynamic color from the device:
// ColorScheme.fromSeed(
// seedColor: ColorScheme.fromImageProvider(
// image: ...,
// ).primary,
// )
ColorScheme.fromSeed generates a harmonious scheme. On Android 12+, the system provides wallpaper-based colors automatically when using useMaterial3: true.
Common Mistakes
Hardcoding colors throughout the app: Every color value should come from ThemeData. Hardcoding prevents easy theme changes and dark mode support.
Not defining both light and dark themes: Without
darkTheme, Flutter falls back to the light theme in dark mode, resulting in poor readability.Using ColorScheme colors incorrectly:
primaryis for prominent UI elements,surfacefor backgrounds,onPrimaryfor text on primary backgrounds. Mixing them up causes contrast issues.Forgetting ThemeMode.system: Default to
ThemeMode.systemso the app respects the device's dark mode setting.Overriding theme in every widget: Set themes globally in MaterialApp. Override only for specific exceptions using
Themewidget wrapping.
Practice Questions
- How does
colorSchemeSeedgenerate a complete color scheme? - What is the purpose of ThemeExtension?
- How do
themeanddarkThemeparameters work in MaterialApp? - When would you use component themes vs inline widget styles?
- Challenge: Create a brand theme for a coffee shop app with brown primary, cream surface, and green accent colors. Implement both light and dark variants. Add a custom ThemeExtension for brand-specific colors like
coffeeandcream.
Mini Project
Build a theme showcase app:
- Define a complete theme with custom colors, typography, and component themes
- Implement light and dark modes
- Add a theme switcher in settings
- Display all typography styles in a preview screen
- Show themed versions of common components (buttons, cards, inputs, dialogs)
- Create a custom ThemeExtension for status colors
- Persist the theme preference with SharedPreferences
FAQ
What is Next
Now that you understand theming, learn about animations. Proceed to Flutter Animations for implicit and explicit animations, transitions, and AnimatedBuilder. Then explore Flutter Local Storage for persisting data locally.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro