Updated June 2026. Tested on Flutter 3.x and Dart 3. Part of the Techalyst Flutter series.

Hardcoding colours and font sizes into widgets is fine for a demo and miserable for a real app. The day you want a dark mode, or the brand colour changes, you are hunting through every file. Flutter's theme system fixes this by giving you one place to define how the app looks and a way to read it from anywhere. With Material 3, setting it up takes only a few lines.

A theme from one seed colour

Material 3 is the current default, and its best trick is generating a whole coordinated palette from a single seed colour. You pick one colour, and ColorScheme.fromSeed derives the primary, secondary, surface, error and on-colours that go with it.

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
    useMaterial3: true,
  ),
  home: const HomeScreen(),
)

That one colorScheme now drives the colours of buttons, app bars, and the rest, all in harmony, without you choosing a dozen shades by hand.

Read the theme, do not hardcode

The habit that makes theming pay off is reading colours and text styles from the theme instead of writing literal values in your widgets:

@override
Widget build(BuildContext context) {
  final theme = Theme.of(context);

  return Container(
    color: theme.colorScheme.surface,
    child: Text('Welcome', style: theme.textTheme.titleLarge),
  );
}

Theme.of(context) walks up the tree to your ThemeData, the same context lookup we cover in the widget tree and BuildContext. Because the colour comes from the theme rather than a hardcoded Colors.blue, switching to dark mode or changing the brand colour updates this widget for free. Every Colors.something you bake into a widget is a spot dark mode will break.

Dark mode

A dark theme is the same seed with a dark brightness, and you let the device decide which one is active:

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.indigo,
      brightness: Brightness.dark,
    ),
  ),
  themeMode: ThemeMode.system, // follow the OS setting
)

themeMode: ThemeMode.system follows the phone's light or dark setting automatically. Set it to ThemeMode.light or ThemeMode.dark if you want a manual toggle instead. As long as your widgets read from the theme, both modes just work.

Styling a component everywhere at once

When you want every button or app bar to share a look, set it once in the theme rather than styling each instance:

ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
    ),
  ),
  appBarTheme: const AppBarTheme(centerTitle: true),
)

Now every ElevatedButton is rounded and every AppBar centres its title, with no per-widget styling. Change it here and it changes everywhere.

Wrapping up

Theming keeps a Flutter app consistent and ready for dark mode without effort. Generate a palette with ColorScheme.fromSeed from one seed colour, read colours and text through Theme.of(context) instead of hardcoding them, add a dark theme with brightness: Brightness.dark and let themeMode follow the system, and set component themes to style buttons and bars in one place. Do that from the start and restyling the whole app later is a one-line change, not a search-and-replace marathon.