0Pricing
Flutter Mobile Development · Lesson

Material 3 Color Schemes and Seed Colors

Generate harmonious color schemes from a single seed color using ColorScheme.fromSeed.

Material 3 Color Schemes and Seed Colors is a free Flutter Mobile Development lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Flutter Mobile Development learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Is a Color Scheme in Material 3?

Material 3 (M3) introduced a structured color scheme — a set of named color roles that your entire app uses consistently. Instead of scattering raw hex values across widgets, you define roles like primary, onPrimary, surface, error, and Flutter's theming engine applies them automatically.

  • primary — the brand color used for buttons, FABs, active tabs
  • onPrimary — text/icons drawn on top of primary
  • surface — background of cards, sheets, dialogs
  • onSurface — text drawn on surfaces
  • error / onError — validation and alert colors

Every role comes in both a light and a dark variant, letting you support both themes with one color scheme definition.

The Problem ColorScheme.fromSeed Solves

Before M3, developers had to hand-pick every color role — primary, secondary, tertiary, surfaces, containers, and their 'on' counterparts. That's 25+ values, and getting accessible contrast ratios right manually is error-prone.

ColorScheme.fromSeed solves this by accepting a single seed color and using Google's HCT color space algorithm (Hue, Chroma, Tone) to generate a full, harmonious, WCAG-accessible palette automatically.

  • One input → 25+ output roles
  • Guaranteed contrast between foreground and background pairs
  • Light and dark variants from the same seed
  • Consistent with the Material You design language

Your First ColorScheme.fromSeed

Using ColorScheme.fromSeed is straightforward. Pass it to ThemeData inside your MaterialApp. Flutter does the rest.

The seedColor parameter accepts any Color — typically your brand color. Flutter then derives the entire palette from it.

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'M3 Theming Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4), // brand purple
        ),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Color Scheme Demo')),
      body: const Center(child: Text('Hello Material 3')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.palette),
      ),
    );
  }
}

Light and Dark from the Same Seed

One of the biggest wins of ColorScheme.fromSeed is that you can generate both a light and a dark theme from the same seed color, ensuring visual consistency across modes.

Pass the brightness parameter to switch between them. Flutter adjusts tones automatically — dark themes use lighter tones of your seed on dark surfaces.

import 'package:flutter/material.dart';

const Color _brandColor = Color(0xFF00897B); // teal brand

ThemeData lightTheme() => ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: _brandColor,
        brightness: Brightness.light,
      ),
      useMaterial3: true,
    );

ThemeData darkTheme() => ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: _brandColor,
        brightness: Brightness.dark,
      ),
      useMaterial3: true,
    );

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: lightTheme(),
      darkTheme: darkTheme(),
      themeMode: ThemeMode.system, // follows device setting
      home: const Scaffold(
        body: Center(child: Text('Adaptive theme')),
      ),
    );
  }
}

Accessing Color Roles in Widgets

Once your ColorScheme is set up, access its roles anywhere via Theme.of(context).colorScheme. This keeps your widgets decoupled from hard-coded colors and automatically respects light/dark mode.

Common roles you will use in widgets:

  • colorScheme.primary — highlight color for interactive elements
  • colorScheme.onPrimary — text/icon color on primary backgrounds
  • colorScheme.surface — card and dialog backgrounds
  • colorScheme.onSurface — default text color
  • colorScheme.error — error state indicators
import 'package:flutter/material.dart';

class StatusBadge extends StatelessWidget {
  final String label;
  final bool isActive;

  const StatusBadge({super.key, required this.label, required this.isActive});

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

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: isActive ? cs.primaryContainer : cs.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: TextStyle(
          color: isActive ? cs.onPrimaryContainer : cs.onSurfaceVariant,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

Container Roles: primaryContainer and Friends

M3 introduced container roles — softer, lower-chroma versions of core colors designed for backgrounds of UI elements like chips, cards, and filled buttons.

  • primaryContainer — soft primary background (great for selected chips, filled tonal buttons)
  • onPrimaryContainer — text on top of primaryContainer
  • secondaryContainer / tertiaryContainer — analogous roles for secondary and tertiary palettes
  • errorContainer — soft red for inline error banners

Using containers instead of raw primary for backgrounds gives you visual hierarchy and keeps contrast ratios safe without manually adjusting opacity.

import 'package:flutter/material.dart';

class InfoCard extends StatelessWidget {
  final String title;
  final String body;

  const InfoCard({super.key, required this.title, required this.body});

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

    return Card(
      color: cs.primaryContainer,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    color: cs.onPrimaryContainer,
                    fontWeight: FontWeight.bold,
                  ),
            ),
            const SizedBox(height: 8),
            Text(
              body,
              style: TextStyle(color: cs.onPrimaryContainer),
            ),
          ],
        ),
      ),
    );
  }
}

Overriding Specific Roles

Sometimes the algorithmically generated color for a specific role doesn't match your brand guide exactly. You can override individual roles by using ColorScheme.fromSeed(...).copyWith(...). This keeps all the auto-generated roles while letting you pin specific ones.

This pattern is useful when, for example, your design system requires an exact red for error that differs from what the HCT algorithm produces.

import 'package:flutter/material.dart';

ThemeData buildTheme() {
  // Start from seed, then pin specific roles
  final base = ColorScheme.fromSeed(
    seedColor: const Color(0xFF1565C0), // deep blue brand
  );

  return ThemeData(
    colorScheme: base.copyWith(
      // Exact error red from our design system
      error: const Color(0xFFB00020),
      onError: Colors.white,
      // Pin tertiary to our accent orange
      tertiary: const Color(0xFFE65100),
      onTertiary: Colors.white,
    ),
    useMaterial3: true,
  );
}

Dynamic Color: Adapting to Wallpaper

Android 12+ supports Dynamic Color — the system extracts a seed color from the user's wallpaper and applies it to all apps. Flutter supports this via the dynamic_color package.

The pattern is: try to load the system color scheme; fall back to your own seed if unavailable (iOS, older Android, or when the user hasn't set a wallpaper).

This makes your app feel native and personalized on supported devices while remaining consistent everywhere else.

// pubspec.yaml: dynamic_color: ^1.7.0

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

const Color _fallbackSeed = Color(0xFF6750A4);

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

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        final lightScheme = lightDynamic ??
            ColorScheme.fromSeed(
              seedColor: _fallbackSeed,
              brightness: Brightness.light,
            );

        final darkScheme = darkDynamic ??
            ColorScheme.fromSeed(
              seedColor: _fallbackSeed,
              brightness: Brightness.dark,
            );

        return MaterialApp(
          theme: ThemeData(colorScheme: lightScheme, useMaterial3: true),
          darkTheme: ThemeData(colorScheme: darkScheme, useMaterial3: true),
          themeMode: ThemeMode.system,
          home: const Scaffold(
            body: Center(child: Text('Dynamic Color')),
          ),
        );
      },
    );
  }
}

Previewing Your Palette with a Color Swatch Grid

While developing, it's useful to render all key color roles on-screen so you can verify the generated palette visually. This debug screen is a quick way to check contrast and harmony before shipping.

Build a simple grid that maps role names to their colors and 'on' counterparts, using the live ColorScheme from context.

import 'package:flutter/material.dart';

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

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

    final roles = [
      ('Primary', cs.primary, cs.onPrimary),
      ('PrimaryContainer', cs.primaryContainer, cs.onPrimaryContainer),
      ('Secondary', cs.secondary, cs.onSecondary),
      ('SecondaryContainer', cs.secondaryContainer, cs.onSecondaryContainer),
      ('Tertiary', cs.tertiary, cs.onTertiary),
      ('Surface', cs.surface, cs.onSurface),
      ('Error', cs.error, cs.onError),
      ('ErrorContainer', cs.errorContainer, cs.onErrorContainer),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Color Palette')),
      body: ListView(
        children: roles.map((r) {
          final (name, bg, fg) = r;
          return Container(
            height: 56,
            color: bg,
            alignment: Alignment.centerLeft,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(name, style: TextStyle(color: fg, fontWeight: FontWeight.w500)),
          );
        }).toList(),
      ),
    );
  }
}

Common Pitfalls with Color Schemes

Avoid these mistakes when working with M3 color schemes:

  • Hard-coding hex colors in widgets — Always use Theme.of(context).colorScheme.* so widgets respond to theme changes and dark mode.
  • Using Colors.white / Colors.black directly — These break in dark mode. Use colorScheme.onSurface or colorScheme.surface instead.
  • Forgetting useMaterial3: true — Without this flag, Flutter uses M2 defaults and your M3 color roles may not be applied correctly to built-in widgets.
  • Using primaryColor from ThemeData — That's an M2 concept. In M3, all widgets read from colorScheme. Set ThemeData.colorScheme, not ThemeData.primaryColor.
  • Mismatching brightness and background — Generating a Brightness.dark scheme but pairing it with a white scaffold will produce unreadable UI.

Putting It All Together: A Themed App Shell

Here is a complete minimal app shell that demonstrates best practices: seed-based color scheme, light/dark support, useMaterial3: true, and widgets that read roles from context — no hard-coded colors anywhere.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

const Color _seed = Color(0xFF2E7D32); // forest green brand

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Green Theme',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _seed,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _seed,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Scaffold(
      backgroundColor: cs.surface,
      appBar: AppBar(
        title: Text('Dashboard', style: TextStyle(color: cs.onPrimary)),
        backgroundColor: cs.primary,
      ),
      body: Center(
        child: FilledButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.eco),
          label: const Text('Go Green'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        backgroundColor: cs.tertiary,
        foregroundColor: cs.onTertiary,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Quick Check: ColorScheme.fromSeed

Test your understanding of how ColorScheme.fromSeed works in Material 3.

Lesson Recap: Material 3 Color Schemes and Seed Colors

Here's what you learned in this lesson:

  • ColorScheme in M3 defines a full set of named color roles (primary, surface, error, containers, and their 'on' counterparts) that Flutter widgets consume automatically.
  • ColorScheme.fromSeed generates all 25+ roles from a single brand color using Google's HCT algorithm, guaranteeing harmony and accessibility.
  • Pass brightness: Brightness.light or Brightness.dark to generate appropriate variants from the same seed — always pair with MaterialApp.darkTheme for full dark mode support.
  • Access roles in widgets via Theme.of(context).colorScheme — never hard-code hex values in widget files.
  • Use container roles (primaryContainer, onPrimaryContainer) for backgrounds of UI components; they provide visual hierarchy without manual opacity tweaks.
  • Override individual roles with .copyWith() when a specific brand color must be exact.
  • On Android 12+, Dynamic Color via the dynamic_color package extracts the system seed; always provide a fallback for other platforms.
  • Always set useMaterial3: true on ThemeData so built-in widgets read from the M3 color scheme correctly.

Frequently asked questions

Is the “Material 3 Color Schemes and Seed Colors” lesson free?

Yes — the full text of “Material 3 Color Schemes and Seed Colors” is free to read here on the web, and the Flutter Mobile Development course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Flutter Mobile Development course, upgrade to CoddyKit PRO.

What will I learn in “Material 3 Color Schemes and Seed Colors”?

Generate harmonious color schemes from a single seed color using ColorScheme.fromSeed. You practise Flutter Mobile Development with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Flutter Mobile Development?

No prior experience is required. Flutter Mobile Development on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Material 3 Color Schemes and Seed Colors” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Flutter Mobile Development lesson?

Yes. Every Flutter Mobile Development lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Material 3 Color Schemes and Seed Colors
  2. Dynamic Color and Adaptive Light/Dark Themes
  3. Custom ThemeExtension for Brand Tokens
  4. Responsive Typography and Component Theming
← Back to Flutter Mobile Development