ThemeExtension colours for light and dark
Custom semantic colours (success, warning, surface tints) added to ThemeData with a ThemeExtension that lerps between modes.
FlutterThemingflutterthemedarkmodematerial3
Code
import 'package:flutter/material.dart';
@immutable
class AppColors extends ThemeExtension<AppColors> {
const AppColors({required this.success, required this.warning, required this.priceTag});
final Color success, warning, priceTag;
static const light = AppColors(
success: Color(0xFF1B7F4B),
warning: Color(0xFFB26A00),
priceTag: Color(0xFFE8F1FF),
);
static const dark = AppColors(
success: Color(0xFF6FD6A0),
warning: Color(0xFFFFB84D),
priceTag: Color(0xFF1F2A3D),
);
@override
AppColors copyWith({Color? success, Color? warning, Color? priceTag}) => AppColors(
success: success ?? this.success,
warning: warning ?? this.warning,
priceTag: priceTag ?? this.priceTag,
);
@override
AppColors lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
// Lets theme changes cross-fade smoothly during AnimatedTheme.
return AppColors(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
priceTag: Color.lerp(priceTag, other.priceTag, t)!,
);
}
}
class AppTheme {
static ThemeData build(Brightness b) {
final scheme = ColorScheme.fromSeed(seedColor: const Color(0xFF3057E0), brightness: b);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
extensions: [b == Brightness.dark ? AppColors.dark : AppColors.light],
);
}
}
extension AppColorsX on BuildContext {
AppColors get appColors => Theme.of(this).extension<AppColors>()!;
}
// MaterialApp(
// theme: AppTheme.build(Brightness.light),
// darkTheme: AppTheme.build(Brightness.dark),
// themeMode: ThemeMode.system,
// )
How it works
What it solves
Material's colour scheme has no slot for ideas like success, warning or a tinted price badge. Hard-coding those colours in widgets leaves them unreadable when dark mode switches on.
How it works
AppColors is a ThemeExtension, the supported way to attach extra design tokens to ThemeData. It carries a light and a dark palette as constants; AppTheme.build seeds the Material 3 colour scheme for the requested brightness and registers the matching extension.
copyWith and lerp are required by the contract. lerp blends each colour, which makes the animated theme transition cross-fade the custom colours together with the standard ones. A BuildContext extension offers a short context.appColors accessor that reads from the current theme.
Adapting it
Add tokens by extending the three places: the field, copyWith and lerp. Keep sizes and radii in another extension so colour changes stay cheap. Persist themeMode in local storage and expose a toggle that rebuilds MaterialApp.
Pitfalls
- The null-assertion in
appColorsfails if you forget to register the extension in a customThemeData(for example in tests). - Check contrast for dark values; do not merely invert the light ones.
lerpignores a different extension type by returningthis, which is correct and intentional.
How to use it
Place the file in lib/theme/ and wire it into MaterialApp.
Text('In stock', style: TextStyle(color: context.appColors.success));
Supply theme, darkTheme and themeMode as shown in the trailing comment. No packages.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.