Theming in Flutter is primarily implemented using the ThemeData class and the Theme widget, allowing you to define a consistent look and feel across your entire application.
🎨 Implementing Theming in Flutter
- Define
ThemeData: Create one or more instances of theThemeDataclass, typically for a light and a dark theme. The most important properties to set for a modern theme are:colorScheme: This is the heart of Material Design 3 (M3) theming. UseColorScheme.fromSeed()with a singleseedColorto automatically generate a harmonious color palette that adheres to M3 guidelines.useMaterial3: Set this totrue(it’s the default since Flutter 3.16) to adopt the latest M3 components, typography, and color systems.textTheme: Define the text styling (like font family, size, and weight) for various text roles (e.g.,displayLarge,titleMedium,bodySmall). You can use packages likegoogle_fontshere for custom typography.- Widget Themes (e.g.,
appBarTheme,elevatedButtonTheme): Customize the default appearance of specific widgets beyond the color scheme.
- Apply to
MaterialApp: Pass your light and darkThemeDataobjects to theMaterialAppwidget in your app’s root:MaterialApp( title: 'My Themed App', theme: lightTheme, // Your light ThemeData darkTheme: darkTheme, // Your dark ThemeData themeMode: ThemeMode.system, // Choose light, dark, or follow system setting // ... ); - Access the Theme: Use
Theme.of(context)within your widgets to access the currently active theme data. This ensures your custom widgets automatically adapt to the user’s selected theme (light or dark).Color primaryColor = Theme.of(context).colorScheme.primary; TextStyle bodyStyle = Theme.of(context).textTheme.bodyMedium; - Override Locally: To apply a different theme to a specific part of your widget tree, wrap that section in a
Themewidget and pass a new or modifiedThemeDatainstance to itsdataproperty. You can use.copyWith()on the parent theme data to only override specific attributes.
✨ Modern Themes and Designs (Material Design 3)
The most modern and recommended approach in Flutter is to embrace Material Design 3 (M3), often referred to as “Material You.” Key design aspects of modern Flutter apps built on M3 include:
- Dynamic Color: Use
ColorScheme.fromSeed()to generate an entire color scheme based on a single brand color (the “seed”). This palette includes light and dark variations with accessible contrast. - Emphasis on
ColorScheme: Modern theming primarily relies on the M3ColorSchemeroles (likeprimary,secondary,surface,onPrimary, etc.) rather than deprecated properties likeprimaryColorandaccentColor. - Larger, Expressive Typography: The M3
TextThemeuses a more refined set of styles (likedisplayLarge,headlineMedium,titleSmall) and generally favors larger, more open typefaces for titles and headers. - Rounded Shapes: Most M3 components, like buttons, cards, and dialogs, feature more rounded corners than the previous Material Design versions, adding a friendly, organic feel.
- Elevation and Surface Color: Components use different surface colors and subtle shadows (elevation) to create visual hierarchy, which adapts distinctively in dark mode.
For real-world modern Flutter design examples, you can look to apps that follow Google’s current design language, such as Google Pay, Google Classroom, or showcase projects featured on the Flutter website. For visual inspiration, designer communities often share modern Flutter UI concepts.
The video below demonstrates how to use the ThemeData widget to apply a theme.
This video provides a basic introduction to the ThemeData widget, which is fundamental to implementing theming in Flutter. https://www.youtube.com/watch?v=TkNG9I8g6iY
A second read
You could also implement theming using an InheritedWidget or a ThemeExtension (the modern, preferred way).
1. The Modern Way: ThemeExtension (Recommended)
ThemeExtension is the most modern and clean way to add custom, strongly-typed properties to your existing ThemeData. This allows your custom values to be part of the standard theming system, supporting light/dark mode and context resolution.
Step 1: Define the Extension Class
Create a class that extends ThemeExtension<T> and includes all your custom, contextually parameterized values. It must implement the required copyWith and lerp methods for smooth transitions.
import 'package:flutter/material.dart';
@immutable
class CustomAppColors extends ThemeExtension<CustomAppColors> {
const CustomAppColors({
required this.brandColor,
required this.warningIconColor,
});
final Color brandColor;
final Color warningIconColor;
// Required: Create a copy of the extension with optional new values
@override
CustomAppColors copyWith({
Color? brandColor,
Color? warningIconColor,
}) {
return CustomAppColors(
brandColor: brandColor ?? this.brandColor,
warningIconColor: warningIconColor ?? this.warningIconColor,
);
}
// Required: Interpolate (transition) between two theme extensions
@override
CustomAppColors lerp(
covariant ThemeExtension<CustomAppColors>? other,
double t,
) {
if (other is! CustomAppColors) {
return this;
}
return CustomAppColors(
brandColor: Color.lerp(brandColor, other.brandColor, t)!,
warningIconColor: Color.lerp(warningIconColor, other.warningIconColor, t)!,
);
}
}Step 2: Add the Extension to ThemeData
Apply the custom extension to your light and dark themes using the extensions property in ThemeData.
final lightTheme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
extensions: const <ThemeExtension<dynamic>>[
CustomAppColors(
brandColor: Colors.purple, // Light mode value
warningIconColor: Colors.orange,
),
],
);
final darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
extensions: const <ThemeExtension<dynamic>>[
CustomAppColors(
brandColor: Colors.lightBlueAccent, // Dark mode value
warningIconColor: Colors.redAccent,
),
],
);Step 3: Read Values from Context
Access your custom values anywhere in your app using Theme.of(context).extension<T>()!.
class MyCustomWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. Retrieve the extension from the current theme (which is determined by context)
final customColors = Theme.of(context).extension<CustomAppColors>()!;
return Container(
// 2. Use the contextually parameterized value
color: customColors.brandColor,
child: Icon(
Icons.warning,
color: customColors.warningIconColor,
),
);
}
}2. The Traditional Way: InheritedWidget
For simpler, more isolated contextual data that doesn’t need to integrate with ThemeData‘s light/dark switching or animation (lerp), you can use an InheritedWidget (or a state management solution that uses it, like Provider or Riverpod).
- Define the Widget: Create a custom
InheritedWidgetto hold your data. - Wrap the Tree: Wrap the part of the widget tree that needs access to the values with this widget.
- Read the Values: Use
context.dependOnInheritedWidgetOfExactType<T>()in any descendant widget to retrieve the values based on its location in the tree.
Key Difference: ThemeExtension makes your custom values global to the MaterialApp theme, while a manually placed InheritedWidget can be used to provide local, overridden, contextual parameters to a subtree.
