iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Theme & Material 3

A Flutter ThemeData is your design system in one place: colors, typography, shape, density, component defaults. Pair it with Material 3’s ColorScheme.fromSeed and a runtime light/dark toggle and you ship consistent, accessible UI with almost no per-screen styling.

ThemeData, ColorScheme, dark, custom

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

// 1) Minimum viable theme
class App extends StatelessWidget {
    const App({super.key});
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'My App',
            theme: ThemeData(
                useMaterial3: true,
                colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5)),
            ),
            darkTheme: ThemeData(
                useMaterial3: true,
                colorScheme: ColorScheme.fromSeed(
                    seedColor:  const Color(0xFF4F46E5),
                    brightness: Brightness.dark,
                ),
            ),
            themeMode: ThemeMode.system,           // honour OS
            home: const HomePage(),
        );
    }
}

// ColorScheme.fromSeed generates a full Material 3 palette from one seed color — guaranteed accessible contrast.

// 2) Reading the current theme inside a widget
class HomePage extends StatelessWidget {
    const HomePage({super.key});
    @override
    Widget build(BuildContext context) {
        final theme = Theme.of(context);
        final cs    = theme.colorScheme;
        final tt    = theme.textTheme;

        return Scaffold(
            appBar: AppBar(title: const Text('Home')),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                        Text('Welcome', style: tt.headlineLarge),
                        const SizedBox(height: 8),
                        Text('Theme-driven typography.', style: tt.bodyMedium?.copyWith(color: cs.onSurfaceVariant)),
                        const SizedBox(height: 24),
                        FilledButton(onPressed: () {}, child: const Text('Get started')),
                    ],
                ),
            ),
        );
    }
}

// 3) Customising components via *Theme objects
final theme = ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5)),
    appBarTheme: const AppBarTheme(
        centerTitle:   true,
        elevation:     0,
        scrolledUnderElevation: 1,
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
        style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
            shape:   RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        ),
    ),
    inputDecorationTheme: InputDecorationTheme(
        filled:        true,
        border:        OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
        contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    ),
    cardTheme: CardTheme(
        elevation: 0,
        shape:     RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
        color:     const Color(0xFFF8F9FB),
    ),
    snackBarTheme: const SnackBarThemeData(
        behavior: SnackBarBehavior.floating,
    ),
);

// 4) Custom typography
final textTheme = const TextTheme(
    headlineLarge:  TextStyle(fontFamily: 'Inter', fontSize: 32, fontWeight: FontWeight.w700),
    headlineMedium: TextStyle(fontFamily: 'Inter', fontSize: 24, fontWeight: FontWeight.w600),
    bodyLarge:      TextStyle(fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w400),
    bodyMedium:     TextStyle(fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w400),
);

ThemeData(textTheme: textTheme, /* … */);

// pubspec.yaml — register fonts
// flutter:
//   fonts:
//     - family: Inter
//       fonts:
//         - asset: assets/fonts/Inter-Regular.ttf
//         - asset: assets/fonts/Inter-Bold.ttf
//           weight: 700

// 5) Runtime light/dark toggle
class ThemeController extends ChangeNotifier {
    ThemeMode _mode = ThemeMode.system;
    ThemeMode get mode => _mode;
    void set(ThemeMode m) { _mode = m; notifyListeners(); }
    void toggle() { set(_mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark); }
}

final controller = ThemeController();

class App2 extends StatelessWidget {
    const App2({super.key});
    @override
    Widget build(BuildContext context) {
        return AnimatedBuilder(
            animation: controller,
            builder: (_, __) => MaterialApp(
                theme:    ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5))),
                darkTheme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF4F46E5), brightness: Brightness.dark)),
                themeMode: controller.mode,
                home: const HomePage(),
            ),
        );
    }
}

// 6) Brand variants — keep colors out of widgets
class BrandColors {
    final Color accent;
    final Color accentOn;
    final Color success;
    final Color danger;
    BrandColors({ required this.accent, required this.accentOn, required this.success, required this.danger });

    static const indigo = BrandColors._(accent: Color(0xFF4F46E5), accentOn: Colors.white, success: Color(0xFF16A34A), danger: Color(0xFFDC2626));
    const BrandColors._({required this.accent, required this.accentOn, required this.success, required this.danger });
}

extension BrandColorsExt on ThemeData {
    BrandColors get brand => extension<_BrandColorsExt>()!.brand;
}

class _BrandColorsExt extends ThemeExtension<_BrandColorsExt> {
    final BrandColors brand;
    const _BrandColorsExt(this.brand);
    @override ThemeExtension<_BrandColorsExt> copyWith({ BrandColors? brand }) => _BrandColorsExt(brand ?? this.brand);
    @override ThemeExtension<_BrandColorsExt> lerp(covariant _BrandColorsExt? other, double t) => this;
}

final theme2 = ThemeData(useMaterial3: true).copyWith(
    extensions: <ThemeExtension<dynamic>>[const _BrandColorsExt(BrandColors.indigo)],
);

// Use it
class SuccessLabel extends StatelessWidget {
    const SuccessLabel({super.key, required this.text});
    final String text;
    @override
    Widget build(BuildContext context) {
        final brand = Theme.of(context).brand;
        return Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
            decoration: BoxDecoration(color: brand.success, borderRadius: BorderRadius.circular(999)),
            child: Text(text, style: const TextStyle(color: Colors.white)),
        );
    }
}

// 7) Density + visual density
final compact = ThemeData(visualDensity: const VisualDensity(horizontal: -2, vertical: -2));
final cozy    = ThemeData(visualDensity: VisualDensity.standard);
final comfortable = ThemeData(visualDensity: VisualDensity.comfortable);
// Useful for tablet + phone variants of the same screen.

// 8) Cupertino + Material together
import 'package:flutter/cupertino.dart';
MaterialApp(
    theme: ThemeData(useMaterial3: true),
    cupertinoOverrideTheme: const CupertinoThemeData(
        primaryColor: Color(0xFF4F46E5),
    ),
    /* … */
);

// 9) Reading platform brightness when MaterialApp isn't above you
final brightness = MediaQuery.platformBrightnessOf(context);
final isDark = brightness == Brightness.dark;

// 10) Component-level overrides
Theme(
    data: Theme.of(context).copyWith(
        elevatedButtonTheme: ElevatedButtonThemeData(
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
        ),
    ),
    child: const ElevatedButton(onPressed: null, child: Text('Danger')),
)

// 11) Material 3 vs 2
// • Material 3 (useMaterial3: true) — current Google design language; pill buttons, larger touch targets
// • Material 2 — older look; only use if you absolutely cannot migrate (some packages still ship M2 widgets)
// • ColorScheme.fromSeed is M3-only; M2 used ColorScheme.fromSwatch / MaterialColor

// 12) Accessibility & contrast
// fromSeed picks accessible contrast pairs automatically.
// For custom colors, check WCAG manually:
final contrast = ThemeData.estimateBrightnessForColor(const Color(0xFF4F46E5));
// Or use the dynamic_color package on Android 12+ to source the palette from the user's wallpaper.

// 13) Theming with a state-management library (Riverpod)
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);

class MyApp extends ConsumerWidget {
    @override
    Widget build(BuildContext context, WidgetRef ref) {
        return MaterialApp(
            theme: lightTheme,
            darkTheme: darkTheme,
            themeMode: ref.watch(themeModeProvider),
            home: const HomePage(),
        );
    }
}

// 14) Common bugs
// • Defining ThemeData inside build() — runs every rebuild, allocates new objects; build it once and reuse
// • Hard-coded Colors.x in widgets — bypasses theming; consume from cs / brand instead
// • Mixing useMaterial3: true and Material 2 component themes (ButtonTheme without ElevatedButtonTheme) — visual mismatch
// • Forgetting darkTheme → app ignores OS dark mode
// • Custom font not loading — pubspec missing weight / italic variants; check console for 'Could not find a set of Noto fonts' style errors
// • Theme.of inside a context above MaterialApp — returns the default ThemeData, not yours
// • Long pixel sizes in app-level theme overrides — prefer relative values (textTheme.copyWith(...))

Why it matters

Define ThemeData once with useMaterial3: true and ColorScheme.fromSeed, configure component defaults (button shape, input decoration, app bar) so screens stay terse, and ship a runtime light/dark toggle that honours ThemeMode.system. Custom design tokens belong in a ThemeExtension so they ride along with light/dark just like the built-in colors.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
MaterialApp(
    theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.green,
        textTheme: const TextTheme(
            titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
        ),
    ),
)
Try it Yourself »

Discussion

Loading…