Theme & Material 3
A Flutter ThemeData is your design system in one place: colors, typography, shape, density, component defaults. Pair it with Material 3’s ColorScheme.fromSeed and a runtime light/dark toggle and you ship consistent, accessible UI with almost no per-screen styling.
ThemeData, ColorScheme, dark, custom
EXAMPLE
import 'package:flutter/material.dart';
// 1) Minimum viable theme
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5)),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF4F46E5),
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system, // honour OS
home: const HomePage(),
);
}
}
// ColorScheme.fromSeed generates a full Material 3 palette from one seed color — guaranteed accessible contrast.
// 2) Reading the current theme inside a widget
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cs = theme.colorScheme;
final tt = theme.textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Welcome', style: tt.headlineLarge),
const SizedBox(height: 8),
Text('Theme-driven typography.', style: tt.bodyMedium?.copyWith(color: cs.onSurfaceVariant)),
const SizedBox(height: 24),
FilledButton(onPressed: () {}, child: const Text('Get started')),
],
),
),
);
}
}
// 3) Customising components via *Theme objects
final theme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5)),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
cardTheme: CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
color: const Color(0xFFF8F9FB),
),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
);
// 4) Custom typography
final textTheme = const TextTheme(
headlineLarge: TextStyle(fontFamily: 'Inter', fontSize: 32, fontWeight: FontWeight.w700),
headlineMedium: TextStyle(fontFamily: 'Inter', fontSize: 24, fontWeight: FontWeight.w600),
bodyLarge: TextStyle(fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w400),
bodyMedium: TextStyle(fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400),
);
ThemeData(textTheme: textTheme, /* … */);
// pubspec.yaml — register fonts
// flutter:
// fonts:
// - family: Inter
// fonts:
// - asset: assets/fonts/Inter-Regular.ttf
// - asset: assets/fonts/Inter-Bold.ttf
// weight: 700
// 5) Runtime light/dark toggle
class ThemeController extends ChangeNotifier {
ThemeMode _mode = ThemeMode.system;
ThemeMode get mode => _mode;
void set(ThemeMode m) { _mode = m; notifyListeners(); }
void toggle() { set(_mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark); }
}
final controller = ThemeController();
class App2 extends StatelessWidget {
const App2({super.key});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (_, __) => MaterialApp(
theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5))),
darkTheme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5), brightness: Brightness.dark)),
themeMode: controller.mode,
home: const HomePage(),
),
);
}
}
// 6) Brand variants — keep colors out of widgets
class BrandColors {
final Color accent;
final Color accentOn;
final Color success;
final Color danger;
BrandColors({ required this.accent, required this.accentOn, required this.success, required this.danger });
static const indigo = BrandColors._(accent: Color(0xFF4F46E5), accentOn: Colors.white, success: Color(0xFF16A34A), danger: Color(0xFFDC2626));
const BrandColors._({required this.accent, required this.accentOn, required this.success, required this.danger });
}
extension BrandColorsExt on ThemeData {
BrandColors get brand => extension<_BrandColorsExt>()!.brand;
}
class _BrandColorsExt extends ThemeExtension<_BrandColorsExt> {
final BrandColors brand;
const _BrandColorsExt(this.brand);
@override ThemeExtension<_BrandColorsExt> copyWith({ BrandColors? brand }) => _BrandColorsExt(brand ?? this.brand);
@override ThemeExtension<_BrandColorsExt> lerp(covariant _BrandColorsExt? other, double t) => this;
}
final theme2 = ThemeData(useMaterial3: true).copyWith(
extensions: <ThemeExtension<dynamic>>[const _BrandColorsExt(BrandColors.indigo)],
);
// Use it
class SuccessLabel extends StatelessWidget {
const SuccessLabel({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
final brand = Theme.of(context).brand;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(color: brand.success, borderRadius: BorderRadius.circular(999)),
child: Text(text, style: const TextStyle(color: Colors.white)),
);
}
}
// 7) Density + visual density
final compact = ThemeData(visualDensity: const VisualDensity(horizontal: -2, vertical: -2));
final cozy = ThemeData(visualDensity: VisualDensity.standard);
final comfortable = ThemeData(visualDensity: VisualDensity.comfortable);
// Useful for tablet + phone variants of the same screen.
// 8) Cupertino + Material together
import 'package:flutter/cupertino.dart';
MaterialApp(
theme: ThemeData(useMaterial3: true),
cupertinoOverrideTheme: const CupertinoThemeData(
primaryColor: Color(0xFF4F46E5),
),
/* … */
);
// 9) Reading platform brightness when MaterialApp isn't above you
final brightness = MediaQuery.platformBrightnessOf(context);
final isDark = brightness == Brightness.dark;
// 10) Component-level overrides
Theme(
data: Theme.of(context).copyWith(
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
),
),
child: const ElevatedButton(onPressed: null, child: Text('Danger')),
)
// 11) Material 3 vs 2
// • Material 3 (useMaterial3: true) — current Google design language; pill buttons, larger touch targets
// • Material 2 — older look; only use if you absolutely cannot migrate (some packages still ship M2 widgets)
// • ColorScheme.fromSeed is M3-only; M2 used ColorScheme.fromSwatch / MaterialColor
// 12) Accessibility & contrast
// fromSeed picks accessible contrast pairs automatically.
// For custom colors, check WCAG manually:
final contrast = ThemeData.estimateBrightnessForColor(const Color(0xFF4F46E5));
// Or use the dynamic_color package on Android 12+ to source the palette from the user's wallpaper.
// 13) Theming with a state-management library (Riverpod)
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp(
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ref.watch(themeModeProvider),
home: const HomePage(),
);
}
}
// 14) Common bugs
// • Defining ThemeData inside build() — runs every rebuild, allocates new objects; build it once and reuse
// • Hard-coded Colors.x in widgets — bypasses theming; consume from cs / brand instead
// • Mixing useMaterial3: true and Material 2 component themes (ButtonTheme without ElevatedButtonTheme) — visual mismatch
// • Forgetting darkTheme → app ignores OS dark mode
// • Custom font not loading — pubspec missing weight / italic variants; check console for 'Could not find a set of Noto fonts' style errors
// • Theme.of inside a context above MaterialApp — returns the default ThemeData, not yours
// • Long pixel sizes in app-level theme overrides — prefer relative values (textTheme.copyWith(...))
Why it matters
Define ThemeData once with useMaterial3: true and ColorScheme.fromSeed, configure component defaults (button shape, input decoration, app bar) so screens stay terse, and ship a runtime light/dark toggle that honours ThemeMode.system. Custom design tokens belong in a ThemeExtension so they ride along with light/dark just like the built-in colors.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.green,
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
),
),
)
Try it Yourself »
Discussion
Loading…