You are working on theming for a Flutter app. The patterns below assume a shared design_system/ package holding cross-app tokens (DSColors, DSSpacing, DSRadius, DSTypography), with each app free to keep its own lib/theme/ for app-specific overrides.
Always prefer importing a token from the design system over duplicating the value in an app.
Workflow: create
Scaffold a complete theme for the target app, taken from the request or the skill argument. If it is not clear which app, ask before writing anything: each one has its own theme directory and overwriting the wrong app is silent.
Theme Directory Structure
<app>/lib/shared/theme/
├── app_theme.dart ← ThemeData factory (light/dark)
├── app_colors.dart ← App-specific color overrides (imports DSColors)
└── app_text_styles.dart ← App-specific text style overrides (imports DSTypography)
app_colors.dart
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// App-specific color aliases. Delegates to DSColors for shared tokens
/// and defines overrides only when this app diverges from the design system.
abstract final class AppColors {
// Re-export shared tokens for convenience
static const Color primary = DSColors.brandPrimary;
static const Color secondary = DSColors.brandSecondary;
static const Color error = DSColors.error;
static const Color success = DSColors.success;
static const Color warning = DSColors.warning;
// App-specific overrides
static const Color scaffoldBackground = Color(0xFFF8F9FA);
static const Color cardBackground = DSColors.neutral0;
static const Color divider = DSColors.neutral200;
}
app_text_styles.dart
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
/// App-specific text style overrides. Use DSTypography directly when possible.
abstract final class AppTextStyles {
static TextStyle get pageTitle => DSTypography.headingH4;
static TextStyle get sectionTitle => DSTypography.headingH5;
static TextStyle get body => DSTypography.body14Regular;
static TextStyle get caption => DSTypography.caption;
// App-specific additions only
static TextStyle get price => DSTypography.price;
}
app_theme.dart (Material 3)
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'app_colors.dart';
class AppTheme {
static ThemeData light() {
final colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: 'Roboto',
scaffoldBackgroundColor: AppColors.scaffoldBackground,
// --- Component Themes (use *ThemeData suffix) ---
appBarTheme: AppBarThemeData(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
centerTitle: false,
),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: DSRadius.lgBorderRadius,
side: BorderSide(color: AppColors.divider),
),
color: AppColors.cardBackground,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: DSColors.neutral0,
shape: RoundedRectangleBorder(
borderRadius: DSRadius.mdBorderRadius,
),
minimumSize: const Size(double.infinity, 48),
textStyle: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: DSColors.neutral0,
shape: RoundedRectangleBorder(
borderRadius: DSRadius.mdBorderRadius,
),
minimumSize: const Size(double.infinity, 48),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: BorderSide(color: AppColors.primary),
shape: RoundedRectangleBorder(
borderRadius: DSRadius.mdBorderRadius,
),
minimumSize: const Size(double.infinity, 48),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: DSRadius.mdBorderRadius,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: DSSpacing.lg,
vertical: DSSpacing.md,
),
),
navigationBarTheme: NavigationBarThemeData(
indicatorColor: colorScheme.secondaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return DSTypography.label.copyWith(color: AppColors.primary);
}
return DSTypography.label;
}),
),
dividerTheme: DividerThemeData(
color: AppColors.divider,
thickness: 1,
space: 0,
),
// --- Typography ---
textTheme: const TextTheme(
displayLarge: DSTypography.headingH4,
headlineMedium: DSTypography.headingH5,
titleLarge: DSTypography.headingH6,
bodyLarge: DSTypography.body16Regular,
bodyMedium: DSTypography.body14Regular,
bodySmall: DSTypography.body12Regular,
labelSmall: DSTypography.label,
),
);
}
static ThemeData dark() {
final colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: 'Roboto',
// Mirror the light theme structure but with dark-appropriate values.
// Override specific component themes as needed.
);
}
}
Applying the Theme
In app_widget.dart:
MaterialApp(
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: ThemeMode.system,
// ...
)
Workflow: migrate
Migrate an existing app to Material 3 theming. Read the app's current app_widget.dart and any existing theme files first.
Migration Checklist
- Enable Material 3: Add
useMaterial3: truetoThemeData - Replace
primarySwatchwithcolorScheme: ColorScheme.fromSeed(seedColor: ...) - Apply deprecated replacements:
| Deprecated | Replacement |
|---|---|
accentColor |
colorScheme.secondary |
primaryColor (in ThemeData) |
colorScheme.primary |
AppBarTheme.color |
AppBarThemeData.backgroundColor |
AppBarTheme.brightness |
AppBarThemeData.systemOverlayStyle |
BottomNavigationBar |
NavigationBar |
FlatButton |
TextButton |
RaisedButton |
ElevatedButton |
ButtonTheme |
TextButtonThemeData / ElevatedButtonThemeData / OutlinedButtonThemeData |
MaterialStateProperty |
WidgetStateProperty |
MaterialState |
WidgetState |
- Normalize component theme class names (use
*ThemeDatasuffix):
| Old Name | Correct Name |
|---|---|
CardTheme(...) constructor in ThemeData |
CardThemeData(...) |
AppBarTheme(...) |
AppBarThemeData(...) |
IconTheme(...) in ThemeData |
IconThemeData(...) |
TabBarTheme(...) |
TabBarThemeData(...) |
DialogTheme(...) |
DialogThemeData(...) |
BottomSheetTheme(...) |
BottomSheetThemeData(...) |
TooltipTheme(...) |
TooltipThemeData(...) |
- Replace hardcoded colors with
DSColorsorTheme.of(context).colorSchemereferences - Update typography to Material 3 scale:
| Old (Material 2) | New (Material 3) |
|---|---|
headline1 |
displayLarge |
headline2 |
displayMedium |
headline3 |
displaySmall |
headline4 |
headlineMedium |
headline5 |
headlineSmall |
headline6 |
titleLarge |
subtitle1 |
titleMedium |
subtitle2 |
titleSmall |
bodyText1 |
bodyLarge |
bodyText2 |
bodyMedium |
caption |
bodySmall |
button |
labelLarge |
overline |
labelSmall |
Button Styling After Migration
// ElevatedButton with full styling
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
disabledBackgroundColor: DSColors.neutral200,
shape: RoundedRectangleBorder(
borderRadius: DSRadius.mdBorderRadius,
),
padding: const EdgeInsets.symmetric(
horizontal: DSSpacing.xl,
vertical: DSSpacing.md,
),
),
onPressed: () {},
child: const Text('Action'),
)
// FilledButton (Material 3 preferred primary action)
FilledButton(
onPressed: () {},
child: const Text('Primary Action'),
)
// FilledButton.tonal (secondary emphasis)
FilledButton.tonal(
onPressed: () {},
child: const Text('Secondary Action'),
)
State-Dependent Styling
// Use WidgetStateProperty (NOT MaterialStateProperty) for state-dependent styles
ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) return DSColors.neutral200;
if (states.contains(WidgetState.pressed)) return AppColors.primary.withValues(alpha: 0.8);
if (states.contains(WidgetState.hovered)) return AppColors.primary.withValues(alpha: 0.9);
return AppColors.primary;
}),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) return DSColors.neutral500;
return DSColors.neutral0;
}),
overlayColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.pressed)) return Colors.white.withValues(alpha: 0.12);
if (states.contains(WidgetState.hovered)) return Colors.white.withValues(alpha: 0.08);
return null;
}),
)
// Shorthand for simple cases
WidgetStateProperty.all(DSColors.neutral0)
Workflow: audit
Audit theme consistency across apps and report issues.
Audit Steps
- Read every app's root widget (
app_widget.dart/main.dart) - Read all files under
*/lib/theme/,*/lib/shared/theme/ - Search for hardcoded
Color(0x...)values in widget files (outside theme files) - Search for deprecated APIs:
accentColor,primarySwatch,FlatButton,RaisedButton,MaterialState - Check that all apps import from
design_systeminstead of duplicating token values - Verify
useMaterial3: trueis set in all apps
Report Format
Produce a markdown report with:
## Theme Audit
### Material 3 Status
- <app>: [enabled/disabled] (one line per app)
### Deprecated API Usage
- [file:line] — description of deprecated usage and fix
### Hardcoded Colors (should use DSColors/AppColors)
- [file:line] — Color(0xFF...) → suggested token
### Token Duplication
- [file] duplicates [token] already in design_system
### Recommendations
1. ...
Instructions
- Always read the app's existing
app_widget.dartand theme files before making changes - Import shared tokens from
design_system— never duplicateDSColors,DSSpacing,DSRadius, orDSTypographyvalues - Use
abstract final classfor all token/color/style classes (cannot be instantiated) - All token values must be
static const - Use
ColorScheme.fromSeed()— never constructColorScheme()manually unless overriding specific slots - Use
*ThemeDatasuffix for component themes (CardThemeData,AppBarThemeData, etc.) - Use
WidgetStatePropertyandWidgetState— neverMaterialStatePropertyorMaterialState - Use
.withValues(alpha: 0.5)— never.withOpacity(0.5)(deprecated) - Prefer
FilledButtonfor primary actions overElevatedButton(Material 3 convention) - Always set
useMaterial3: true - When the app already has an app-local
<App>DesignTokens-style class, migrate its values into thedesign_systempackage or intoapp_colors.dart/app_text_styles.dart— do not leave duplicate token files