Themes & Styling

Sprinkling colors and font sizes directly into every single widget works for a demo, but it falls apart fast in a real app — change the brand color, and suddenly you're hunting through dozens of files. ThemeData is Flutter's answer: a single, central place to define your app's colors, typography, and component styling once, which every widget in the tree can then read from consistently.

ThemeData and MaterialApp

A ThemeData object is handed to MaterialApp through its theme: property. Once set there, it's automatically available to every widget beneath it in the tree — you never have to pass it down manually.

MaterialApp(
  title: 'My App',
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
    useMaterial3: true,
  ),
  home: const HomeScreen(),
)

A Full Worked ThemeData Example

Material 3's modern approach doesn't ask you to pick a dozen individual colors by hand — instead, ColorScheme.fromSeed() takes a single seed color and algorithmically derives an entire harmonious palette from it (primary, secondary, surface, error, and their "on-" counterparts for text/icons that sit on top of them). You can then layer a custom textTheme on top for typography.

final appTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.deepPurple,
    brightness: Brightness.light,
  ),
  textTheme: const TextTheme(
    headlineSmall: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
    bodyMedium: TextStyle(fontSize: 15, height: 1.4),
    bodySmall: TextStyle(fontSize: 12, color: Colors.grey),
  ),
  appBarTheme: const AppBarTheme(
    centerTitle: true,
    elevation: 0,
  ),
);
Note: Almost every major Material widget — AppBar, ElevatedButton, Card, TextField — has its own *Theme class (AppBarTheme, ElevatedButtonTheme, and so on) that can be set inside ThemeData to style every instance of that widget app-wide at once.

Reading the Theme with Theme.of(context)

Once a theme is set on MaterialApp, any widget in the tree can read it back with Theme.of(context) — the same .of(context) pattern covered in The Widget Tree & BuildContext. Under the hood, it's walking up the widget tree from the current BuildContext to find the nearest Theme (itself backed by an InheritedWidget) and returning its ThemeData.

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

  return Text(
    'Welcome back',
    style: theme.textTheme.headlineSmall?.copyWith(
      color: theme.colorScheme.primary,
    ),
  );
}

Dark Mode Support

Supporting dark mode properly is just a second ThemeData plus one property. Define a darkTheme: alongside theme: on MaterialApp, then control which one is active with themeMode:.

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.deepPurple,
      brightness: Brightness.light,
    ),
    useMaterial3: true,
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.deepPurple,
      brightness: Brightness.dark,
    ),
    useMaterial3: true,
  ),
  // Follows the device's system setting automatically
  themeMode: ThemeMode.system,
  home: const HomeScreen(),
)

ThemeMode.system is usually the right default — the app switches automatically whenever the user toggles dark mode at the OS level. Use ThemeMode.light or ThemeMode.dark instead when you want to force one appearance regardless of the device's setting, for example to let the user override it manually from an in-app settings screen (a choice you'd typically persist with shared_preferences so it survives a restart).

One-Off Styling on Individual Widgets

Theming shines for anything that should look consistent across the whole app, but not every style decision belongs in ThemeData. When a single widget needs a genuine one-off override — a uniquely colored warning banner, one specially styled promotional button — it's perfectly fine, and often clearer, to style that widget directly instead of bending the global theme around a single exception.

Container(
  padding: const EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.orange.shade100,
    borderRadius: BorderRadius.circular(8),
  ),
  child: const Text(
    'Your session will expire soon',
    style: TextStyle(color: Colors.deepOrange, fontWeight: FontWeight.w600),
  ),
)
Guideline: if a style is used in more than one place, or represents your app's general look and feel, put it in ThemeData. If it's a genuine one-off exception, style that single widget directly.

Custom Typography with Google Fonts

ThemeData's default typography relies on the system font, which looks fine but generic. The google_fonts package is the common way to bring in custom typography from Google's font library without manually downloading font files and registering them in pubspec.yaml — a heavier, manual approach briefly mentioned in Images & Assets. google_fonts fetches and caches the font for you at runtime, and slots directly into a TextTheme.

ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
  textTheme: GoogleFonts.poppinsTextTheme(),
)
Next: You now have every piece needed to build a polished, real screen. Continue to UI Designs: Login Screen — a full hands-on example putting theming, layout, and forms together in one real screen.