12 KiB
12 KiB
Color Schemes - M3 Expressive
Overview
Material 3 Expressive uses color purposefully to guide attention and create emotional impact beyond aesthetics. The key difference is bolder use of on-container colors.
Core Principles
- Color with Purpose: Guide user attention, not just decoration
- Vibrant Containers: Bolder on-container colors than standard M3
- Dynamic Color: Support Material You on Android 12+
- Accessibility: Maintain WCAG contrast ratios
Generating Color Schemes
Basic Seed Color Approach
// Generate from single seed color
final lightScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6200EE), // Purple
brightness: Brightness.light,
);
final darkScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6200EE),
brightness: Brightness.dark,
);
Dynamic Color (Material You)
import 'package:dynamic_color/dynamic_color.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
// Use system colors if available, fallback to custom
ColorScheme lightScheme = lightDynamic ?? ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.light,
);
ColorScheme darkScheme = darkDynamic ?? ColorScheme.fromSeed(
seedColor: Colors.deepPurple,
brightness: Brightness.dark,
);
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: lightScheme,
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: darkScheme,
),
);
},
);
}
}
Custom Expressive Scheme
// Create a vibrant, expressive color scheme
ColorScheme createExpressiveScheme({
required Color seedColor,
required Brightness brightness,
}) {
// Generate base scheme
final baseScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: brightness,
);
// Enhance for expressiveness
if (brightness == Brightness.light) {
return baseScheme.copyWith(
// Make primary container more vibrant
primaryContainer: seedColor.withOpacity(0.3),
onPrimaryContainer: seedColor.withOpacity(0.9),
// Boost secondary container
secondaryContainer: baseScheme.secondary.withOpacity(0.25),
onSecondaryContainer: baseScheme.secondary,
// Enhance tertiary for variety
tertiaryContainer: baseScheme.tertiary.withOpacity(0.25),
onTertiaryContainer: baseScheme.tertiary,
);
} else {
return baseScheme.copyWith(
// Dark mode expressive adjustments
primaryContainer: seedColor.withOpacity(0.25),
onPrimaryContainer: seedColor.withOpacity(0.7),
secondaryContainer: baseScheme.secondary.withOpacity(0.2),
onSecondaryContainer: baseScheme.secondary.withOpacity(0.8),
tertiaryContainer: baseScheme.tertiary.withOpacity(0.2),
onTertiaryContainer: baseScheme.tertiary.withOpacity(0.8),
);
}
}
// Usage:
ThemeData(
useMaterial3: true,
colorScheme: createExpressiveScheme(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
)
M3 Expressive Color Roles
Container Colors (Key for Expressiveness)
// Primary containers - main interactive elements
Container(
color: colorScheme.primaryContainer,
child: Text(
'Primary Action',
style: TextStyle(color: colorScheme.onPrimaryContainer),
),
)
// Secondary containers - supportive elements
Container(
color: colorScheme.secondaryContainer,
child: Text(
'Secondary',
style: TextStyle(color: colorScheme.onSecondaryContainer),
),
)
// Tertiary containers - accent elements
Container(
color: colorScheme.tertiaryContainer,
child: Text(
'Tertiary',
style: TextStyle(color: colorScheme.onTertiaryContainer),
),
)
Surface Colors (Backgrounds & Cards)
// Surface hierarchy (from lowest to highest elevation)
colorScheme.surface
colorScheme.surfaceContainerLowest
colorScheme.surfaceContainerLow
colorScheme.surfaceContainer
colorScheme.surfaceContainerHigh
colorScheme.surfaceContainerHighest
Using flex_color_scheme
import 'package:flex_color_scheme/flex_color_scheme.dart';
// Expressive scheme with FlexColorScheme
ThemeData(
useMaterial3: true,
colorScheme: FlexColorScheme.light(
scheme: FlexScheme.indigo,
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
blendLevel: 20, // More color blending for expressiveness
).toScheme,
)
// Or create fully custom:
ThemeData(
useMaterial3: true,
colorScheme: FlexColorScheme.light(
primary: const Color(0xFF6200EE),
secondary: const Color(0xFF03DAC6),
tertiary: const Color(0xFFFF6E40),
surfaceMode: FlexSurfaceMode.highScaffoldLowSurface,
blendLevel: 25,
).toScheme,
)
Color Application Strategies
Strategy 1: Vibrant & Bold
// For energetic, playful apps
final expressiveVibrant = ColorScheme.fromSeed(
seedColor: Colors.deepOrange,
brightness: Brightness.light,
).copyWith(
primaryContainer: Colors.deepOrange.shade100,
onPrimaryContainer: Colors.deepOrange.shade900,
secondaryContainer: Colors.amber.shade100,
onSecondaryContainer: Colors.amber.shade900,
);
Strategy 2: Calm & Professional
// For professional, trustworthy apps
final expressiveCalm = ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
).copyWith(
primaryContainer: Colors.blue.shade50,
onPrimaryContainer: Colors.blue.shade900,
secondaryContainer: Colors.blueGrey.shade50,
onSecondaryContainer: Colors.blueGrey.shade900,
);
Strategy 3: Warm & Friendly
// For welcoming, approachable apps
final expressiveWarm = ColorScheme.fromSeed(
seedColor: Colors.pink,
brightness: Brightness.light,
).copyWith(
primaryContainer: Colors.pink.shade50,
onPrimaryContainer: Colors.pink.shade900,
secondaryContainer: Colors.orange.shade50,
onSecondaryContainer: Colors.orange.shade900,
);
Accessibility Considerations
Contrast Checking
import 'package:flutter/material.dart';
double calculateContrast(Color foreground, Color background) {
final fgLuminance = foreground.computeLuminance();
final bgLuminance = background.computeLuminance();
final lighter = fgLuminance > bgLuminance ? fgLuminance : bgLuminance;
final darker = fgLuminance > bgLuminance ? bgLuminance : fgLuminance;
return (lighter + 0.05) / (darker + 0.05);
}
// WCAG AA requires 4.5:1 for normal text, 3:1 for large text
bool hasGoodContrast(Color foreground, Color background) {
return calculateContrast(foreground, background) >= 4.5;
}
Ensuring Accessible Expressive Colors
ColorScheme ensureAccessibleScheme(ColorScheme scheme) {
// Check and adjust if needed
if (!hasGoodContrast(scheme.onPrimaryContainer, scheme.primaryContainer)) {
// Darken onPrimaryContainer or lighten primaryContainer
return scheme.copyWith(
onPrimaryContainer: scheme.onPrimaryContainer.withOpacity(0.95),
);
}
return scheme;
}
Theme Switcher
class ThemeController extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
Color _seedColor = Colors.deepPurple;
ThemeMode get themeMode => _themeMode;
Color get seedColor => _seedColor;
void setThemeMode(ThemeMode mode) {
_themeMode = mode;
notifyListeners();
}
void setSeedColor(Color color) {
_seedColor = color;
notifyListeners();
}
ColorScheme getLightScheme() {
return ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.light,
);
}
ColorScheme getDarkScheme() {
return ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.dark,
);
}
}
// In app:
ChangeNotifierProvider(
create: (_) => ThemeController(),
child: Consumer<ThemeController>(
builder: (context, themeController, _) {
return MaterialApp(
themeMode: themeController.themeMode,
theme: ThemeData(
useMaterial3: true,
colorScheme: themeController.getLightScheme(),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: themeController.getDarkScheme(),
),
);
},
),
)
Color Picker for Runtime Changes
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
class ColorPickerDialog extends StatefulWidget {
final Color initialColor;
final ValueChanged<Color> onColorChanged;
const ColorPickerDialog({
super.key,
required this.initialColor,
required this.onColorChanged,
});
@override
State<ColorPickerDialog> createState() => _ColorPickerDialogState();
}
class _ColorPickerDialogState extends State<ColorPickerDialog> {
late Color _currentColor;
@override
void initState() {
super.initState();
_currentColor = widget.initialColor;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Pick a seed color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: _currentColor,
onColorChanged: (color) => setState(() => _currentColor = color),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
widget.onColorChanged(_currentColor);
Navigator.pop(context);
},
child: const Text('Apply'),
),
],
);
}
}
Debugging Colors
class ColorDebugger extends StatelessWidget {
final ColorScheme colorScheme;
const ColorDebugger({super.key, required this.colorScheme});
@override
Widget build(BuildContext context) {
return ListView(
children: [
_buildColorTile('Primary', colorScheme.primary, colorScheme.onPrimary),
_buildColorTile('Primary Container', colorScheme.primaryContainer,
colorScheme.onPrimaryContainer),
_buildColorTile('Secondary', colorScheme.secondary,
colorScheme.onSecondary),
_buildColorTile('Secondary Container', colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer),
// ... add all color roles
],
);
}
Widget _buildColorTile(String label, Color background, Color foreground) {
return Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: foreground, fontWeight: FontWeight.bold)),
Text('BG: ${background.value.toRadixString(16)}',
style: TextStyle(color: foreground, fontSize: 12)),
Text('FG: ${foreground.value.toRadixString(16)}',
style: TextStyle(color: foreground, fontSize: 12)),
Text('Contrast: ${calculateContrast(foreground, background).toStringAsFixed(2)}:1',
style: TextStyle(color: foreground, fontSize: 12)),
],
),
);
}
}
Best Practices
- Start with Seed: Use ColorScheme.fromSeed() as foundation
- Test Both Modes: Always check light AND dark themes
- Verify Contrast: Use accessibility checker tools
- Respect System: Support dynamic color when available
- Be Purposeful: Each color should guide user attention
- Stay Consistent: Use color roles, not hardcoded values
Resources
- Material Theme Builder: https://m3.material.io/theme-builder
- WCAG Contrast Checker: https://webaim.org/resources/contrastchecker/
- flex_color_scheme docs: https://pub.dev/packages/flex_color_scheme