Design System & Theming
Consistent Look with ThemeData and Custom Design Systems
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Design system and theming in Flutter means using ThemeData to define your app's visual language: ColorScheme for colors, TextTheme for typography, component themes for widgets, and ThemeExtension for custom properties. Dark mode support comes from defining both theme and darkTheme. Advanced projects like team_mvp_kit use a custom BaseDesignSystem class to centralize all design tokens.
Real-world relevance
In team_mvp_kit, a BaseDesignSystem abstract class defines all visual constants. Concrete implementations (LightDesignSystem, DarkDesignSystem) provide theme-specific values. The toThemeData() method generates the ThemeData passed to MaterialApp. Every widget reads from Theme.of(context) instead of hardcoding values, making theme changes instant and global.
Key points
- ThemeData Basics — ThemeData is Flutter's built-in theming object. It defines colors, text styles, button themes, input decoration themes, and more for your entire app. Pass it to MaterialApp's theme property and every widget in your app automatically uses these defaults. Change one value in ThemeData and it updates everywhere.
- ColorScheme — ColorScheme is the modern way to define colors in Flutter. It replaces the old primarySwatch approach. ColorScheme defines primary, secondary, surface, background, error, and their 'on' variants (text color on top of each). Use ColorScheme.fromSeed() for automatic palette generation from a single color.
- TextTheme — TextTheme defines a consistent set of text styles for your app: displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium, etc. Define them once in ThemeData and use them everywhere via Theme.of(context).textTheme. This ensures consistent typography across all screens.
- Dark Mode Support — Support dark mode by defining both a theme and darkTheme in MaterialApp. Set themeMode to ThemeMode.system to follow the device setting, ThemeMode.light to force light, or ThemeMode.dark to force dark. ColorScheme.fromSeed with Brightness.dark auto-generates dark color variants.
- Component Themes — ThemeData lets you theme individual component types: ElevatedButtonThemeData, InputDecorationTheme, CardTheme, AppBarTheme, etc. Set these once and all instances of that widget type inherit the styling. This eliminates repetitive style: ElevatedButton.styleFrom(...) on every button.
- Custom Design System Class — For advanced projects, create a custom design system class that extends ThemeData with app-specific tokens. In team_mvp_kit, BaseDesignSystem defines spacing constants, custom colors, animation durations, and component-specific styles. Access it via a static method or dependency injection.
- Theme Extensions — Flutter 3+ supports ThemeExtension for adding custom properties to ThemeData. Define a class that extends ThemeExtension, add your custom fields, implement copyWith and lerp, then register it with ThemeData.extensions. Access it via Theme.of(context).extension().
- Accessing Theme in Widgets — Use Theme.of(context) to access the current ThemeData anywhere in the widget tree. It returns ThemeData for colors, text styles, and component themes. Use context.watch or listen for reactive theme changes. Always prefer theme values over hardcoded colors for consistency and dark mode support.
Code example
import 'package:flutter/material.dart';
// Custom design system for team_mvp_kit
class AppTheme {
static ThemeData light() {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: _textTheme,
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
centerTitle: true,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: colorScheme.surfaceContainerHighest
.withOpacity(0.3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
cardTheme: CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colorScheme.outlineVariant,
),
),
),
extensions: const [
AppSpacing(),
AppDurations(),
],
);
}
static ThemeData dark() {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
textTheme: _textTheme,
// Same component themes with dark colors...
);
}
static const _textTheme = TextTheme(
headlineLarge: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
bodyLarge: TextStyle(
fontSize: 16,
height: 1.5,
),
bodyMedium: TextStyle(
fontSize: 14,
height: 1.5,
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
);
}
class AppSpacing extends ThemeExtension<AppSpacing> {
final double xs;
final double sm;
final double md;
final double lg;
final double xl;
const AppSpacing({
this.xs = 4,
this.sm = 8,
this.md = 16,
this.lg = 24,
this.xl = 32,
});
@override
AppSpacing copyWith({
double? xs, double? sm, double? md,
double? lg, double? xl,
}) {
return AppSpacing(
xs: xs ?? this.xs, sm: sm ?? this.sm,
md: md ?? this.md, lg: lg ?? this.lg,
xl: xl ?? this.xl,
);
}
@override
AppSpacing lerp(AppSpacing? other, double t) => this;
}
class AppDurations extends ThemeExtension<AppDurations> {
final Duration fast;
final Duration normal;
const AppDurations({
this.fast = const Duration(milliseconds: 200),
this.normal = const Duration(milliseconds: 350),
});
@override
AppDurations copyWith({Duration? fast, Duration? normal}) {
return AppDurations(
fast: fast ?? this.fast,
normal: normal ?? this.normal,
);
}
@override
AppDurations lerp(AppDurations? other, double t) => this;
}
// Usage in MaterialApp:
// MaterialApp(
// theme: AppTheme.light(),
// darkTheme: AppTheme.dark(),
// themeMode: ThemeMode.system,
// )Line-by-line walkthrough
- 1. Import the Material package
- 2.
- 3. Define AppTheme class with static factory methods
- 4. Static light() method creates the light theme
- 5. Generate a ColorScheme from a seed color (purple)
- 6. Set brightness to light
- 7.
- 8. Build the ThemeData object
- 9. Enable Material Design 3
- 10. Apply the generated color scheme
- 11. Apply custom text theme (defined below)
- 12. AppBar theme: surface background, no elevation, centered title
- 13.
- 14.
- 15.
- 16.
- 17. Elevated button theme: full width, 48px height, rounded corners
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26. Input decoration theme: rounded borders, filled background
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33. Card theme: no elevation, rounded with border outline
- 34.
- 35.
- 36.
- 37.
- 38.
- 39. Register custom ThemeExtension classes
- 40. AppSpacing for consistent spacing values
- 41. AppDurations for animation timing
- 42.
- 43.
- 44.
- 45. Static dark() method creates the dark theme
- 46. Same seed color but Brightness.dark for dark palette
- 47.
- 48. ThemeData with dark color scheme
- 49. Same text theme works for both modes
- 50.
- 51.
- 52. Static text theme shared between light and dark
- 53. headlineLarge: 28px bold with tight letter spacing
- 54.
- 55.
- 56.
- 57. titleLarge: 20px semi-bold
- 58.
- 59.
- 60. bodyLarge: 16px with 1.5 line height
- 61.
- 62.
- 63. bodyMedium: 14px with 1.5 line height
- 64.
- 65.
- 66. labelLarge: 14px semi-bold with wide letter spacing
- 67.
- 68.
- 69.
- 70.
- 71. AppSpacing ThemeExtension with spacing scale
- 72. Define xs through xl spacing values as doubles
- 73.
- 74. Default values: 4, 8, 16, 24, 32
- 75.
- 76.
- 77.
- 78. copyWith for creating modified copies
- 79.
- 80.
- 81.
- 82. lerp returns this (spacing does not animate)
- 83.
- 84. AppDurations ThemeExtension for animations
- 85. fast and normal duration values
- 86.
- 87. Default: 200ms fast, 350ms normal
- 88.
- 89. copyWith for modified copies
- 90.
- 91.
- 92.
- 93. lerp returns this (durations do not interpolate)
- 94.
- 95.
- 96. Usage example in MaterialApp
- 97. Apply light and dark themes
- 98. Follow system theme mode
Spot the bug
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Text(
'Hello',
style: TextStyle(
color: Colors.black,
fontSize: 24,
),
),
);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Use Themes to Share Colors and Font Styles (Flutter Official)
- ThemeData Class (Flutter API)
- Material 3 in Flutter (Flutter Official)