Skip to content

Flutter Theming — Custom Themes, Colors, and Dark Mode

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Flutter Theming. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter theming centralizes visual properties like colors, typography, and component styles into ThemeData objects, enabling consistent branding and automatic dark mode support across the entire app.

What Will You Learn

  • Creating a custom theme with ThemeData
  • Color schemes and Material Design 3
  • Typography with TextTheme
  • Dark mode and theme switching
  • Theme extensions for custom properties
  • Overriding themes for specific widgets
  • Adaptive theming for platform differences

Why It Matters

A consistent visual design is critical for professional apps. Flutter's theming system ensures that every widget uses the same colors, fonts, and styles without manual configuration. Theme changes propagate throughout the widget tree automatically. Implementing dark mode with ThemeData requires minimal code and improves user experience. Material Design 3 provides dynamic color generation from wallpapers on Android 12+.

Real-World Use

The DodaTech Flutter app defines a complete theme system with light and dark variants. The theme includes custom color schemes (primary, secondary, tertiary), typography with two font families, component themes for buttons and cards, and a custom AppColors theme extension for brand-specific colors. Users can switch themes from settings, and the change applies instantly.

Learning Path

flowchart LR
  A[Flutter Networking] --> B[Flutter Theming\nYou are here]
  B --> C[Flutter Animations]
  style B fill:#f90,color:#fff

Basic Theme Setup

Define a theme in MaterialApp to apply it to all descendant widgets:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData(
      colorSchemeSeed: Colors.blue,
      useMaterial3: true,
      brightness: Brightness.light,
    ),
    darkTheme: ThemeData(
      colorSchemeSeed: Colors.blue,
      useMaterial3: true,
      brightness: Brightness.dark,
    ),
    home: HomeScreen(),
  ));
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Theming')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Styled with theme', style: Theme.of(context).textTheme.headlineMedium),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {},
              child: Text('Themed Button'),
            ),
          ],
        ),
      ),
    );
  }
}

colorSchemeSeed generates a complete color scheme from a single color. useMaterial3: true enables Material Design 3 components. brightness controls light/dark mode.

Custom Color Scheme

Define a custom color scheme explicitly:

ThemeData customLightTheme = ThemeData(
  useMaterial3: true,
  brightness: Brightness.light,
  colorScheme: ColorScheme(
    brightness: Brightness.light,
    primary: Color(0xFF1565C0),
    onPrimary: Colors.white,
    secondary: Color(0xFF7B1FA2),
    onSecondary: Colors.white,
    tertiary: Color(0xFF2E7D32),
    onTertiary: Colors.white,
    error: Color(0xFFD32F2F),
    onError: Colors.white,
    surface: Color(0xFFF5F5F5),
    onSurface: Color(0xFF212121),
    outline: Color(0xFFBDBDBD),
  ),
);

ThemeData customDarkTheme = ThemeData(
  useMaterial3: true,
  brightness: Brightness.dark,
  colorScheme: ColorScheme(
    brightness: Brightness.dark,
    primary: Color(0xFF90CAF9),
    onPrimary: Color(0xFF0D47A1),
    secondary: Color(0xFFCE93D8),
    onSecondary: Color(0xFF4A148C),
    tertiary: Color(0xFFA5D6A7),
    onTertiary: Color(0xFF1B5E20),
    error: Color(0xFFEF9A9A),
    onError: Color(0xFFB71C1C),
    surface: Color(0xFF303030),
    onSurface: Color(0xFFE0E0E0),
    outline: Color(0xFF616161),
  ),
);

Define all required ColorScheme slots for full control. The generated scheme from colorSchemeSeed is sufficient for most apps.

Typography

Define custom text styles with TextTheme:

ThemeData(
  textTheme: TextTheme(
    displayLarge: TextStyle(
      fontSize: 57,
      fontWeight: FontWeight.w300,
      letterSpacing: -0.25,
    ),
    displayMedium: TextStyle(
      fontSize: 45,
      fontWeight: FontWeight.w400,
      letterSpacing: 0,
    ),
    headlineLarge: TextStyle(
      fontSize: 32,
      fontWeight: FontWeight.w600,
    ),
    headlineMedium: TextStyle(
      fontSize: 28,
      fontWeight: FontWeight.w500,
    ),
    titleLarge: TextStyle(
      fontSize: 22,
      fontWeight: FontWeight.w600,
    ),
    titleMedium: TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w500,
      letterSpacing: 0.15,
    ),
    bodyLarge: TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w400,
      letterSpacing: 0.5,
    ),
    bodyMedium: TextStyle(
      fontSize: 14,
      fontWeight: FontWeight.w400,
      letterSpacing: 0.25,
    ),
    labelLarge: TextStyle(
      fontSize: 14,
      fontWeight: FontWeight.w500,
      letterSpacing: 0.1,
    ),
  ),
)

Use Theme.of(context).textTheme.headlineMedium to apply text styles. The 2023 Material Design typography scale includes display, headline, title, body, and label sizes.

Component Themes

Style individual components with component themes:

ThemeData(
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      backgroundColor: Color(0xFF1565C0),
      foregroundColor: Colors.white,
      elevation: 2,
      padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
      textStyle: TextStyle(
        fontSize: 16,
        fontWeight: FontWeight.w600,
      ),
    ),
  ),
  cardTheme: CardThemeData(
    elevation: 4,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    clipBehavior: Clip.antiAlias,
  ),
  appBarTheme: AppBarTheme(
    centerTitle: true,
    elevation: 0,
    backgroundColor: Color(0xFF1565C0),
    foregroundColor: Colors.white,
  ),
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    filled: true,
    fillColor: Colors.grey.shade50,
    contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  ),
)

Component themes apply to all instances of that widget type. Override individual widgets by applying a style directly.

Theme Extensions

Add custom theme properties with ThemeExtension:

class AppColors extends ThemeExtension<AppColors> {
  final Color success;
  final Color warning;
  final Color info;
  final Color? surfaceTint;

  AppColors({
    required this.success,
    required this.warning,
    required this.info,
    this.surfaceTint,
  });

  @override
  AppColors copyWith({
    Color? success,
    Color? warning,
    Color? info,
    Color? surfaceTint,
  }) {
    return AppColors(
      success: success ?? this.success,
      warning: warning ?? this.warning,
      info: info ?? this.info,
      surfaceTint: surfaceTint ?? this.surfaceTint,
    );
  }

  @override
  AppColors lerp(ThemeExtension<AppColors>? other, double t) {
    if (other is! AppColors) return this;
    return AppColors(
      success: Color.lerp(success, other.success, t)!,
      warning: Color.lerp(warning, other.warning, t)!,
      info: Color.lerp(info, other.info, t)!,
      surfaceTint: Color.lerp(surfaceTint, other.surfaceTint, t),
    );
  }

  static const light = AppColors(
    success: Color(0xFF4CAF50),
    warning: Color(0xFFFFC107),
    info: Color(0xFF2196F3),
  );

  static const dark = AppColors(
    success: Color(0xFF81C784),
    warning: Color(0xFFFFD54F),
    info: Color(0xFF64B5F6),
  );
}

// Usage in theme:
// ThemeData(
//   extensions: [AppColors.light],
// )

// Usage in widget:
// final appColors = Theme.of(context).extension<AppColors>()!;
// Container(color: appColors.success)

ThemeExtension adds custom properties to the theme system. They are accessible via Theme.of(context).extension<T>() and participate in theme switching and hot reload.

Dynamic Theme Switching

Implement theme switching with state management:

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;

  ThemeMode get themeMode => _themeMode;

  void setThemeMode(ThemeMode mode) {
    _themeMode = mode;
    notifyListeners();
  }

  bool get isDarkMode => _themeMode == ThemeMode.dark;
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => ThemeProvider(),
      child: ThemedApp(),
    ),
  );
}

class ThemedApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = context.watch<ThemeProvider>();

    return MaterialApp(
      themeMode: themeProvider.themeMode,
      theme: customLightTheme,
      darkTheme: customDarkTheme,
      home: SettingsScreen(),
    );
  }
}

class SettingsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = context.watch<ThemeProvider>();

    return Scaffold(
      appBar: AppBar(title: Text('Settings')),
      body: ListView(
        children: [
          RadioListTile<ThemeMode>(
            title: Text('Light'),
            value: ThemeMode.light,
            groupValue: themeProvider.themeMode,
            onChanged: (mode) => themeProvider.setThemeMode(mode!),
          ),
          RadioListTile<ThemeMode>(
            title: Text('Dark'),
            value: ThemeMode.dark,
            groupValue: themeProvider.themeMode,
            onChanged: (mode) => themeProvider.setThemeMode(mode!),
          ),
          RadioListTile<ThemeMode>(
            title: Text('System'),
            value: ThemeMode.system,
            groupValue: themeProvider.themeMode,
            onChanged: (mode) => themeProvider.setThemeMode(mode!),
          ),
        ],
      ),
    );
  }
}

ThemeMode.system follows the device setting. ThemeMode.light and ThemeMode.dark override it. The theme change applies instantly because MaterialApp rebuilds when themeMode changes.

Adaptive Theming

Adapt themes to platform conventions:

ThemeData platformAdaptiveTheme(BuildContext context) {
  final isIOS = Theme.of(context).platform == TargetPlatform.iOS;

  return ThemeData(
    useMaterial3: isIOS ? false : true,
    appBarTheme: AppBarTheme(
      elevation: isIOS ? 0 : 2,
      scrolledUnderElevation: isIOS ? 0.5 : 3,
    ),
    navigationBarTheme: NavigationBarThemeData(
      height: isIOS ? 85 : 65,
      labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
    ),
  );
}

Use Theme.of(context).platform to detect the current platform. iOS apps may benefit from Cupertino-style transitions and navigation bars.

Material Design 3 Dynamic Color

Use dynamic color from wallpaper on Android 12+:

ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Color(0xFF1565C0),
    brightness: Brightness.light,
  ),
)

// Or use dynamic color from the device:
// ColorScheme.fromSeed(
//   seedColor: ColorScheme.fromImageProvider(
//     image: ...,
//   ).primary,
// )

ColorScheme.fromSeed generates a harmonious scheme. On Android 12+, the system provides wallpaper-based colors automatically when using useMaterial3: true.

Common Mistakes

  1. Hardcoding colors throughout the app: Every color value should come from ThemeData. Hardcoding prevents easy theme changes and dark mode support.

  2. Not defining both light and dark themes: Without darkTheme, Flutter falls back to the light theme in dark mode, resulting in poor readability.

  3. Using ColorScheme colors incorrectly: primary is for prominent UI elements, surface for backgrounds, onPrimary for text on primary backgrounds. Mixing them up causes contrast issues.

  4. Forgetting ThemeMode.system: Default to ThemeMode.system so the app respects the device's dark mode setting.

  5. Overriding theme in every widget: Set themes globally in MaterialApp. Override only for specific exceptions using Theme widget wrapping.

Practice Questions

  1. How does colorSchemeSeed generate a complete color scheme?
  2. What is the purpose of ThemeExtension?
  3. How do theme and darkTheme parameters work in MaterialApp?
  4. When would you use component themes vs inline widget styles?
  5. Challenge: Create a brand theme for a coffee shop app with brown primary, cream surface, and green accent colors. Implement both light and dark variants. Add a custom ThemeExtension for brand-specific colors like coffee and cream.

Mini Project

Build a theme showcase app:

  • Define a complete theme with custom colors, typography, and component themes
  • Implement light and dark modes
  • Add a theme switcher in settings
  • Display all typography styles in a preview screen
  • Show themed versions of common components (buttons, cards, inputs, dialogs)
  • Create a custom ThemeExtension for status colors
  • Persist the theme preference with SharedPreferences

FAQ

How does ThemeData propagate to child widgets?

MaterialApp wraps the widget tree with an InheritedWidget that stores the ThemeData. Theme.of(context) retrieves it from the nearest ancestor. The Theme widget overrides the theme for a subtree.

Can I have different themes for different parts of the app?

Yes. Wrap a section in Theme(data: Theme.of(context).copyWith(...), child: ...) to override specific properties for that subtree.

What is the difference between colorScheme and theme colors?

colorScheme defines semantic colors (primary, secondary, surface). Older theme color properties (primaryColor, accentColor) are deprecated in Material 3.

How do I use custom fonts in a theme?

Add the font to pubspec.yaml under fonts:, then use TextTheme(displayLarge: TextStyle(fontFamily: 'YourFont')) in the theme.

Does ThemeExtension affect hot reload?

Yes. ThemeExtensions participate in hot reload. Changes to extension properties are reflected immediately without restarting the app.

What is Next

Now that you understand theming, learn about animations. Proceed to Flutter Animations for implicit and explicit animations, transitions, and AnimatedBuilder. Then explore Flutter Local Storage for persisting data locally.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro