DEV3 Technologies home

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 appColors fails if you forget to register the extension in a custom ThemeData (for example in tests).
  • Check contrast for dark values; do not merely invert the light ones.
  • lerp ignores a different extension type by returning this, 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.

Related snippets