ColorScheme.fromSeed: Generating Consistent Flutter Themes from One Color
Generate Material 3 tonal palettes from a single seed color with ColorScheme.fromSeed — brightness, scheme variants, contrast levels, and how to wire it into ThemeData for light and dark mode.
Published on • October 29, 2026
AI Assistant

Pick a brand color. Now pick its container color, the color of text on that container, the outline for cards in dark mode, and the error palette that still passes contrast checks. Hand-picking forty-plus ColorScheme roles is where most Flutter theming projects stall — or worse, produce colors that fail accessibility contrast.
ColorScheme.fromSeed collapses that job into one line. You hand it a single seed color, and Material 3’s color algorithm generates a complete, contrast-aware tonal palette that covers every role in the scheme.
In this tutorial, you will learn how the seed-to-palette pipeline works, the options fromSeed exposes (brightness, dynamicSchemeVariant, contrastLevel, and per-role overrides), how to wire light and dark themes from one color, and a practical theming checklist.
What fromSeed actually does
The Flutter API docs describe the factory like this:
ColorScheme.fromSeed({
required Color seedColor,
Brightness brightness = Brightness.light,
DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot,
double contrastLevel = 0.0,
Color? primary,
Color? onPrimary,
// ... every other ColorScheme role, all optional
})
“Using the seedColor as a starting point,” the docs explain, “a set of tonal palettes are constructed. By default, the tonal palettes are based on the Material 3 Color system and provide all of the ColorScheme colors. These colors are designed to work well together and meet contrast requirements for accessibility.”
Two consequences worth internalizing:
- Your seed color may not appear in the output. “Given the nature of the algorithm, the
seedColormay not wind up as one of theColorSchemecolors.” If you need the exact hex in the UI, pass it explicitly as an override (or use it for a custom widget style), but let the generated roles handle surfaces, containers, and text. - Every role can still be overridden. Any non-null parameter —
primary,surfaceContainer,error, and so on — replaces the generated value for that field. That’s the escape hatch for brand-locked roles like a specific error red.
Under the hood, palette generation comes from the material_color_utilities package, the same HCT-based system Google uses for Material You on Android.
One color, two modes
The brightness parameter is what makes light/dark theming almost free. Define the theme once and derive both modes from the same seed:
import 'package:flutter/material.dart';
const kSeedColor = Color(0xFF6750A4);
ThemeData lightTheme() => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
brightness: Brightness.light,
),
);
ThemeData darkTheme() => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
brightness: Brightness.dark,
),
);
MaterialApp(
theme: lightTheme(),
darkTheme: darkTheme(),
themeMode: ThemeMode.system,
home: const MyHomePage(),
);
Because both schemes derive from the same seed, light and dark mode feel like the same brand — the tonal relationships are preserved, only the tone targets shift. No more maintaining two hand-built palettes that drift apart.
Tuning the algorithm
Scheme variants
dynamicSchemeVariant changes the style of the generated scheme. The docs default is DynamicSchemeVariant.tonalSpot, which “has pastel palettes and won’t be too ‘colorful’ even if the seedColor has a high chroma value.” If the result looks washed out, they recommend trying DynamicSchemeVariant.fidelity, “whose palettes match the seed color.” It’s a one-line experiment with a big visual payoff, especially for saturated brand colors.
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
),
Contrast levels
contrastLevel adjusts the contrast between color pairs such as primary and onPrimary. Per the API docs: 0.0 is the default (normal), -1.0 is the lowest, and 1.0 is the highest — with 0.5 and 1.0 corresponding to the Material Design guideline’s medium and high contrast levels. That gives you an accessibility knob without leaving the API:
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
brightness: Brightness.dark,
contrastLevel: 0.5, // medium contrast
),
Per-role overrides
Need the exact brand red for destructive actions while keeping everything else generated? Pass it in:
colorScheme: ColorScheme.fromSeed(
seedColor: kSeedColor,
error: const Color(0xFFB3261E),
),
Only the overridden role changes; every other role stays on the generated palette.
Applying it through ThemeData
Following Flutter’s themes cookbook, most apps set two properties on ThemeData: colorScheme (the colors) and textTheme (the type styles). The full wiring looks like this:
MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 30, fontStyle: FontStyle.italic),
bodyMedium: TextStyle(fontSize: 16),
),
),
home: const MyHomePage(),
);
In widgets, read the resolved scheme instead of hard-coding colors — Theme.of(context) looks up the nearest Theme in the tree:
Container(
color: Theme.of(context).colorScheme.primary,
child: Text(
'Text with a background color',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).colorScheme.onPrimary),
),
)
Need a different look for one subtree? Wrap it in a Theme widget, and prefer extending the parent with copyWith over replacing it wholesale:
Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
),
child: const FloatingActionButton(onPressed: null, child: Icon(Icons.add)),
)
Theming checklist
- Choose one
seedColorand hoist it to a single constants file — the whole app’s palette hangs off it. - Build
lightTheme()anddarkTheme()with the same seed and differentbrightness; wire them viatheme/darkTheme/themeMode. - Set
useMaterial3: trueso widgets consume the full M3 role set (container colors, surface containers, outlines). - If the palette looks too muted or too loud, try
DynamicSchemeVariant.fidelitybefore hand-picking colors. - For accessibility-sensitive screens, test
contrastLevelat0.5/1.0rather than nudging individual colors. - Override only the roles you must (brand error red, a fixed primary); resist overriding surfaces — the algorithm’s tonal relationships are the whole point.
- In widgets, always resolve colors from
Theme.of(context).colorScheme; never copy hex values from the theme into component code. - Extend themes per-subtree with
Theme.of(context).copyWith(...)instead of constructing a freshThemeData.
Practical takeaways
ColorScheme.fromSeed(seedColor: ..., brightness: ...)generates the full Material 3 role set from one color, with contrast requirements built in.- The seed itself may not survive into the output — override specific roles if you need exact hexes.
dynamicSchemeVariant(defaulttonalSpot, tryfidelity) andcontrastLevel(-1.0to1.0) are your two tuning dials.- One seed powers both light and dark themes; wire them with
theme,darkTheme, andthemeMode: ThemeMode.system. - Keep the seed in one constant, consume colors via
Theme.of(context).colorScheme, and extend (not replace) themes withcopyWith.
Conclusion & Next Steps
You’ve replaced a fragile hand-maintained palette with a deterministic pipeline: one seed in, a contrast-checked light and dark scheme out. Next, explore DynamicSchemeVariant’s other values for different scheme styles, the material_color_utilities package for generating tones directly, and dynamic color sourced from a wallpaper or user-uploaded image.
Sources
- Flutter Cookbook — Use themes to share colors and font styles. https://docs.flutter.dev/cookbook/design/themes
- Flutter API — ColorScheme.fromSeed constructor. https://api.flutter.dev/flutter/material/ColorScheme/ColorScheme.fromSeed.html
- Material Design 3 — The color system. https://m3.material.io