Kodokon kodokon.com

Themes: ThemeData, Material 3 and dark mode

Centralize colors and typography in ThemeData, generate a Material 3 palette with ColorScheme.fromSeed, and offer a dark mode effortlessly.

9 min · 3 questions

Open this lesson in Kodokon

Stopping styling widget by widget is a professional milestone: ThemeData centralizes colors, typography and shapes for the entire app. With Material 3 (enabled by default since Flutter 3.16), everything starts from the ColorScheme: ColorScheme.fromSeed derives a complete, harmonious and accessible palette from a single seed color.

DART
import 'package:flutter/material.dart';

void main() {
  runApp(const ShopApp());
}

class ShopApp extends StatelessWidget {
  const ShopApp({super.key});

  @override
  Widget build(BuildContext context) {
    const seed = Colors.teal;
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: seed,
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: seed,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system,
      home: Scaffold(
        appBar: AppBar(title: const Text('Shop')),
        body: const Center(child: Text('Welcome')),
      ),
    );
  }
}
Two themes generated from a single seed color.

Inside your widgets, don't reinvent anything: read the ambient theme with Theme.of(context). The colorScheme exposes semantic roles (primary, onPrimary, primaryContainer, surface, and so on) and textTheme exposes named styles (titleMedium, bodyLarge, and so on). A widget written this way adapts automatically to the light theme as well as the dark one.

DART
import 'package:flutter/material.dart';

class PriceTag extends StatelessWidget {
  const PriceTag({super.key, required this.price});

  final double price;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    final text = Theme.of(context).textTheme;
    return Container(
      padding: const EdgeInsets.symmetric(
        horizontal: 12,
        vertical: 6,
      ),
      decoration: BoxDecoration(
        color: colors.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        '$price €',
        style: text.titleMedium?.copyWith(
          color: colors.onPrimaryContainer,
        ),
      ),
    );
  }
}
No hard-coded color: the widget follows the active theme.

To go further, configure the component themes: each family of widgets (app bars, buttons, input fields) accepts a dedicated theme in ThemeData. You define your buttons' padding or your titles' centering once, and every instance inherits it without a single local style.

DART
import 'package:flutter/material.dart';

final ThemeData appTheme = ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.teal,
  ),
  appBarTheme: const AppBarTheme(
    centerTitle: true,
    elevation: 0,
  ),
  filledButtonTheme: FilledButtonThemeData(
    style: FilledButton.styleFrom(
      padding: const EdgeInsets.symmetric(
        horizontal: 24,
        vertical: 12,
      ),
    ),
  ),
);
Component settings apply across the whole app.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What does ColorScheme.fromSeed(seedColor: ...) produce?
    • A single, slightly lightened color
    • A complete, coherent Material 3 palette derived from the seed color
    • A gradient for backgrounds
    • A dark theme only
  2. How do you make the app follow the system's light/dark setting?
    • Provide only theme; the rest is automatic
    • Provide theme and darkTheme, and set themeMode to ThemeMode.system
    • Call setState when the brightness changes
    • Read MediaQuery in every widget
  3. Why write color: Theme.of(context).colorScheme.primary rather than color: Colors.blue?
    • It's shorter to write
    • The color adapts to the active theme and stays consistent across the whole app
    • Colors.blue is deprecated in Material 3