Theming & Styling
Introduction to Flutter Theming
Flutter’s theming system provides a powerful, centralized way to define the visual appearance of your entire application. Instead of styling each widget individually, you define a ThemeData object that sets colors, typography, shapes, and component styles app-wide. Every widget in your tree can then access these values through Theme.of(context), ensuring consistency across your entire design system.
With Material 3 (Material Design 3), Flutter introduced a modern theming approach built around ColorScheme and dynamic color. This lesson covers everything you need to build a professional, maintainable theming system.
useMaterial3: false.ThemeData: The Foundation
ThemeData is the central configuration object that defines your app’s visual properties. It’s passed to the theme parameter of MaterialApp.
Basic ThemeData Setup
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Themed App',
theme: ThemeData(
// Material 3 is default in Flutter 3.16+
useMaterial3: true,
// ColorScheme defines all colors used by Material components
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E88E5), // Your brand color
brightness: Brightness.light,
),
// Typography
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
bodyLarge: TextStyle(fontSize: 16),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
// Component-level customization
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
home: const HomeScreen(),
);
}
}
ColorScheme: Material 3 Color System
In Material 3, ColorScheme is the primary way to define colors. It generates a harmonious set of colors from a single seed color, ensuring visual coherence across all components.
ColorScheme.fromSeed
// Generate a complete color scheme from a single seed color
final lightScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4), // Purple seed
brightness: Brightness.light,
);
final darkScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
);
// The generated scheme includes:
// primary, onPrimary, primaryContainer, onPrimaryContainer
// secondary, onSecondary, secondaryContainer, onSecondaryContainer
// tertiary, onTertiary, tertiaryContainer, onTertiaryContainer
// error, onError, errorContainer, onErrorContainer
// surface, onSurface, surfaceVariant, onSurfaceVariant
// outline, outlineVariant, shadow, scrim
// inverseSurface, onInverseSurface, inversePrimary
Custom ColorScheme with Brand Colors
// For precise brand control, customize specific colors
final brandScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1565C0),
).copyWith(
// Override specific colors while keeping harmony
primary: const Color(0xFF1565C0),
secondary: const Color(0xFFFF6F00),
tertiary: const Color(0xFF2E7D32),
error: const Color(0xFFD32F2F),
);
// Or build a fully custom scheme
final customScheme = const ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF1565C0),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFD1E4FF),
onPrimaryContainer: Color(0xFF001D36),
secondary: Color(0xFFFF6F00),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFFFE0B2),
onSecondaryContainer: Color(0xFF2B1700),
surface: Color(0xFFFFFBFF),
onSurface: Color(0xFF1C1B1F),
error: Color(0xFFD32F2F),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFFFFDAD6),
onErrorContainer: Color(0xFF410002),
outline: Color(0xFF79747E),
outlineVariant: Color(0xFFCAC4D0),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
);
ColorScheme.fromSeed() uses the HCT (Hue, Chroma, Tone) color space to generate perceptually uniform colors. The seed color determines the hue, and the algorithm creates harmonious primary, secondary, and tertiary palettes with proper contrast ratios for accessibility.TextTheme: Typography System
Material 3 defines a structured typography system with specific text styles for different purposes. The TextTheme class organizes these into semantic roles.
Complete TextTheme Configuration
import 'package:google_fonts/google_fonts.dart';
final appTextTheme = TextTheme(
// Display styles -- Large, prominent text
displayLarge: GoogleFonts.poppins(
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
),
displayMedium: GoogleFonts.poppins(
fontSize: 45,
fontWeight: FontWeight.w400,
),
displaySmall: GoogleFonts.poppins(
fontSize: 36,
fontWeight: FontWeight.w400,
),
// Headline styles -- Section headers
headlineLarge: GoogleFonts.poppins(
fontSize: 32,
fontWeight: FontWeight.w600,
),
headlineMedium: GoogleFonts.poppins(
fontSize: 28,
fontWeight: FontWeight.w600,
),
headlineSmall: GoogleFonts.poppins(
fontSize: 24,
fontWeight: FontWeight.w600,
),
// Title styles -- Card titles, dialogs
titleLarge: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w500,
),
titleMedium: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
),
titleSmall: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
),
// Body styles -- Paragraph text
bodyLarge: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
),
bodyMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
),
bodySmall: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
),
// Label styles -- Buttons, tabs, chips
labelLarge: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
),
labelMedium: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
);
Accessing Theme Data
Once your theme is configured, access it anywhere in the widget tree using Theme.of(context).
Using Theme.of(context)
class ProfileCard extends StatelessWidget {
final String name;
final String email;
const ProfileCard({required this.name, required this.email, super.key});
@override
Widget build(BuildContext context) {
// Access the current theme
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
return Card(
// Card automatically uses theme's surface color and elevation
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
email,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {},
child: const Text('View Profile'),
),
],
),
),
);
}
}
Component-Level Theming
Material 3 allows you to customize the default appearance of every component through theme data. Each component has its own theme class.
Comprehensive Component Theming
ThemeData buildAppTheme() {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1E88E5),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// AppBar theme
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 2,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
// Card theme
cardTheme: CardTheme(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
),
// Input decoration theme
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
// Floating action button theme
floatingActionButtonTheme: FloatingActionButtonThemeData(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// Bottom navigation bar theme
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
showUnselectedLabels: true,
),
// Chip theme
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
// Dialog theme
dialogTheme: DialogTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
);
}
Custom Theme Extensions
When Material’s built-in theme properties aren’t enough, you can create custom theme extensions to add your own semantic colors, spacings, or any other design tokens.
Creating Custom Theme Extensions
// Define your custom theme extension
class AppColors extends ThemeExtension<AppColors> {
final Color? success;
final Color? onSuccess;
final Color? warning;
final Color? onWarning;
final Color? info;
final Color? onInfo;
final Color? cardGradientStart;
final Color? cardGradientEnd;
const AppColors({
this.success,
this.onSuccess,
this.warning,
this.onWarning,
this.info,
this.onInfo,
this.cardGradientStart,
this.cardGradientEnd,
});
@override
AppColors copyWith({
Color? success,
Color? onSuccess,
Color? warning,
Color? onWarning,
Color? info,
Color? onInfo,
Color? cardGradientStart,
Color? cardGradientEnd,
}) {
return AppColors(
success: success ?? this.success,
onSuccess: onSuccess ?? this.onSuccess,
warning: warning ?? this.warning,
onWarning: onWarning ?? this.onWarning,
info: info ?? this.info,
onInfo: onInfo ?? this.onInfo,
cardGradientStart: cardGradientStart ?? this.cardGradientStart,
cardGradientEnd: cardGradientEnd ?? this.cardGradientEnd,
);
}
@override
AppColors lerp(AppColors? other, double t) {
if (other is! AppColors) return this;
return AppColors(
success: Color.lerp(success, other.success, t),
onSuccess: Color.lerp(onSuccess, other.onSuccess, t),
warning: Color.lerp(warning, other.warning, t),
onWarning: Color.lerp(onWarning, other.onWarning, t),
info: Color.lerp(info, other.info, t),
onInfo: Color.lerp(onInfo, other.onInfo, t),
cardGradientStart: Color.lerp(cardGradientStart, other.cardGradientStart, t),
cardGradientEnd: Color.lerp(cardGradientEnd, other.cardGradientEnd, t),
);
}
}
// Register the extension in ThemeData
final theme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
extensions: [
const AppColors(
success: Color(0xFF4CAF50),
onSuccess: Color(0xFFFFFFFF),
warning: Color(0xFFFFC107),
onWarning: Color(0xFF000000),
info: Color(0xFF2196F3),
onInfo: Color(0xFFFFFFFF),
cardGradientStart: Color(0xFF1E88E5),
cardGradientEnd: Color(0xFF7C4DFF),
),
],
);
// Access the extension anywhere
class StatusBanner extends StatelessWidget {
final String message;
final StatusType type;
const StatusBanner({required this.message, required this.type, super.key});
@override
Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!;
final (bgColor, fgColor) = switch (type) {
StatusType.success => (appColors.success!, appColors.onSuccess!),
StatusType.warning => (appColors.warning!, appColors.onWarning!),
StatusType.info => (appColors.info!, appColors.onInfo!),
};
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(message, style: TextStyle(color: fgColor)),
);
}
}
enum StatusType { success, warning, info }
lerp method is required for smooth theme transitions (e.g., animating between light and dark themes). It interpolates between two instances of your extension at a given factor t (0.0 to 1.0).Scoped Theming with Theme Widget
You can override the theme for a subtree of widgets using the Theme widget. This is useful for sections of your app that need a different visual treatment.
Scoped Theme Override
class PremiumSection extends StatelessWidget {
const PremiumSection({super.key});
@override
Widget build(BuildContext context) {
return Theme(
// Override theme for this subtree only
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFFFD700), // Gold theme
brightness: Theme.of(context).brightness,
),
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFFFFD700), width: 2),
),
),
),
child: Column(
children: [
// These widgets use the gold theme
Card(
child: ListTile(
leading: const Icon(Icons.star),
title: const Text('Premium Feature'),
subtitle: const Text('Exclusive access'),
),
),
FilledButton(
onPressed: () {},
child: const Text('Upgrade Now'),
),
],
),
);
}
}
Practical Example: Complete Brand Theme
Let’s build a complete, production-ready theme that demonstrates all the concepts together:
Production Brand Theme
// lib/theme/app_theme.dart
import 'package:flutter/material.dart';
class AppTheme {
// Brand colors
static const _brandBlue = Color(0xFF1565C0);
static const _brandOrange = Color(0xFFFF6F00);
// Spacing constants (not part of ThemeData, but useful)
static const double spacingXs = 4;
static const double spacingSm = 8;
static const double spacingMd = 16;
static const double spacingLg = 24;
static const double spacingXl = 32;
// Border radius constants
static const double radiusSm = 8;
static const double radiusMd = 12;
static const double radiusLg = 16;
static const double radiusXl = 28;
static ThemeData get lightTheme {
final colorScheme = ColorScheme.fromSeed(
seedColor: _brandBlue,
brightness: Brightness.light,
);
return _buildTheme(colorScheme);
}
static ThemeData get darkTheme {
final colorScheme = ColorScheme.fromSeed(
seedColor: _brandBlue,
brightness: Brightness.dark,
);
return _buildTheme(colorScheme);
}
static ThemeData _buildTheme(ColorScheme colorScheme) {
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// Scaffold
scaffoldBackgroundColor: colorScheme.surface,
// AppBar
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
),
// Cards
cardTheme: CardTheme(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
),
// Buttons
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
// Input fields
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: spacingMd,
vertical: 14,
),
),
// Divider
dividerTheme: DividerThemeData(
color: colorScheme.outlineVariant,
thickness: 1,
space: 1,
),
// Extensions
extensions: [
AppColors(
success: const Color(0xFF4CAF50),
onSuccess: Colors.white,
warning: const Color(0xFFFFC107),
onWarning: Colors.black,
info: colorScheme.primary,
onInfo: colorScheme.onPrimary,
cardGradientStart: colorScheme.primaryContainer,
cardGradientEnd: colorScheme.tertiaryContainer,
),
],
);
}
}
// Usage in MaterialApp
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const HomeScreen(),
);
}
}
Summary
- ThemeData is the central configuration for your app’s visual design, passed to MaterialApp.
- ColorScheme.fromSeed() generates a complete, harmonious color palette from a single seed color.
- TextTheme provides semantic text styles (display, headline, title, body, label) with three sizes each.
- Theme.of(context) gives you access to the current theme anywhere in the widget tree.
- Component themes (AppBarTheme, CardTheme, etc.) customize default styling for specific widgets.
- ThemeExtension lets you add custom design tokens (success colors, spacings, etc.) that integrate with the theme system.
- The Theme widget allows scoped overrides for subtrees that need different styling.
- Organize your theme code in a dedicated class (AppTheme) with static methods for maintainability.