By day 20 of building Flutter apps, you've probably copy-pasted button styles across screens or tweaked colors in multiple places. That's a sign it's time to learn proper theming. Theming with Material 3 gives you a systematic way to define colors, typography, and component styles once, then apply them everywhere—no repetition, no inconsistency, and a clean path to dark mode support.
Why Theming Matters in Production Apps
I've worked on projects where designers approved a color palette at project start, but by month three, we had five slightly different shades of blue scattered across the codebase. When the brand team decided to pivot the primary color, we spent days hunting down hardcoded values. Theming solves this at the architecture level.
In Material 3, your entire app's visual identity flows from a single theme definition. Change one value in your theme, and every button, text field, and card respects it. That's not just convenience—it's professional app development.
ColorScheme.fromSeed — Generate a Full Palette from One Brand Color
Material 3 introduced an elegant feature: generate a complete, harmonious color scheme from a single seed color. Designers love this because it ensures color harmony without manual tweaking.
Here's how it works in practice:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final seedColor = Color(0xFF6200EE); // Your brand primary
return MaterialApp(
title: 'Theming Demo',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system, // Respect device setting
home: const HomeScreen(),
);
}
}That single ColorScheme.fromSeed() call generates your primary, secondary, tertiary, surface, and error colors—all harmonized. I've used this on three production projects, and it's cut color management work by 80%. The generated palette is semantically sound; you're not fighting Material Design principles.
What you get from the seed:
- Primary — Main brand color for high-emphasis actions.
- Secondary — Accent for supporting actions and highlights.
- Tertiary — Additional accent, often for toggles or badges.
- Surface — Background for cards, dialogs, sheets.
- Error — Validation feedback and destructive actions.
If you need to override specific colors, you can pass them directly:
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
error: Color(0xFFB3261E), // Custom error if needed
),Text Themes — Define Typography Once and Reuse
Consistency in typography matters as much as color. On an e-commerce app I worked on, we had body text in five different sizes because developers picked weights ad-hoc. Text themes prevent that.
Material 3 provides a predefined text theme hierarchy, and you extend it in your app theme:
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: seedColor),
textTheme: TextTheme(
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Color(0xFF1C1B1F),
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF1C1B1F),
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
height: 1.5,
color: Color(0xFF1C1B1F),
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Color(0xFF49454E),
),
labelSmall: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
)Then, anywhere in your app, use it like this:
Text(
'Welcome back, User',
style: Theme.of(context).textTheme.displayMedium,
)
Text(
'Order details and shipping information',
style: Theme.of(context).textTheme.bodyMedium,
)This approach couples your UI to the theme definition, not to hardcoded values. When a designer says "make headlines bigger," you change it in one place.
Pro tip: Use Google Fonts for consistency. I usually define my custom text theme in a separate file:
// lib/theme/text_theme.dart
import 'package:google_fonts/google_fonts.dart';
TextTheme buildTextTheme(ColorScheme colorScheme) {
return GoogleFonts.interTextTheme().copyWith(
displayLarge: GoogleFonts.inter(
fontSize: 32,
fontWeight: FontWeight.bold,
color: colorScheme.onBackground,
),
// ... other styles
);
}Component Themes — Style Buttons, Inputs, and Cards Globally
Buttons, text fields, cards, and other components have their own theme properties. Set them once in your Material 3 theme, and every instance uses the same styling by default.
I'll walk through a real example from a trading platform I built:
ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: seedColor),
// Elevated button (primary action)
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
),
// Text button (secondary action)
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: colorScheme.primary,
textStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
),
// Input decoration for text fields
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
hintStyle: TextStyle(color: colorScheme.outlineVariant),
),
// Card theme
cardTheme: CardTheme(
color: colorScheme.surface,
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
)Now, when you build a button or text field, you don't need to repeat styling:
// Button automatically uses elevatedButtonTheme
ElevatedButton(
onPressed: () => placeOrder(),
child: Text('Place Order'),
)
// Text field automatically uses inputDecorationTheme
TextField(
decoration: InputDecoration(
labelText: 'Account Number',
hintText: 'Enter your 10-digit number',
),
)This is production-grade consistency. I've shipped ride-hailing and SaaS platforms with this pattern, and it makes scaling UI across multiple screens painless.
Dark Mode — Provide a Dark Theme and Respect System Settings
Modern apps are expected to support dark mode. With Material 3, it's as simple as defining a second theme for dark brightness.
Here's the complete setup:
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ThemeMode _themeMode = ThemeMode.system;
@override
Widget build(BuildContext context) {
final seedColor = Color(0xFF6200EE);
return MaterialApp(
title: 'Dark Mode App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
),
textTheme: buildTextTheme(
ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
),
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
textTheme: buildTextTheme(
ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
),
),
themeMode: _themeMode,
home: SettingsScreen(
onThemeChanged: (mode) {
setState(() => _themeMode = mode);
},
),
);
}
}The key is themeMode: ThemeMode.system at app startup. This respects the device's system setting. Users can override it in your settings screen:
class SettingsScreen extends StatelessWidget {
final Function(ThemeMode) onThemeChanged;
const SettingsScreen({required this.onThemeChanged});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings')),
body: ListView(
children: [
ListTile(
title: Text('Theme'),
trailing: DropdownButton<ThemeMode>(
value: Theme.of(context).brightness == Brightness.dark
? ThemeMode.dark
: ThemeMode.light,
items: [
DropdownMenuItem(
value: ThemeMode.system,
child: Text('System'),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text('Light'),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text('Dark'),
),
],
onChanged: (mode) {
if (mode != null) onThemeChanged(mode);
},
),
),
],
),
);
}
}For dark mode, Material 3 automatically inverts the color scheme semantics. Your primary color becomes lighter (for readability on dark surfaces), and surfaces darken. You rarely need to manually adjust—the algorithm handles it.
Common Mistakes
- Hardcoding colors in widgets — Developers often write
Color(0xFF6200EE)inside a widget instead of usingTheme.of(context).colorScheme.primary. This breaks theming. Always reach for the theme. - Forgetting useMaterial3: true — If you don't enable Material 3, you'll use the old Material 2 defaults, and the
ColorScheme.fromSeedAPI won't apply correctly. - Duplicating text styles — Don't create custom TextStyle objects for every widget. Define them once in textTheme and reference them. I've debugged apps where developers had 20 different "headline" sizes.
- Not testing dark mode — Define darkTheme but forget to test it. Always verify both light and dark variants work correctly. Surface colors especially need attention.
- Ignoring semanticaly correct colors — Use
onPrimaryfor text on primary backgrounds,onSurfacefor text on surfaces, etc. Don't just guess a color that looks good; Material 3 chose these for accessibility.
Key Takeaways
ColorScheme.fromSeed()generates a harmonious, complete palette from a single brand color—no manual color picking required.- Define all typography in
textThemeonce, then useTheme.of(context).textThemethroughout your app. This ensures consistency and makes global typography changes trivial. - Set component themes (buttons, inputs, cards) in your Material 3 theme definition. Every instance automatically respects the global style.
- Support dark mode by providing a
darkThemewithbrightness: Brightness.dark. UseThemeMode.systemto respect device settings, and let users override in settings. - Always reference theme values via
Theme.of(context)instead of hardcoding. This couples your UI to the theme, not to magic numbers. - Test both light and dark themes thoroughly. Pay attention to contrast and readability, especially with custom colors.






Comments
Loading comments…