Skip to content

ColorToken cannot chain color operations (withValues, withOpacity) #828

@tilucasoli

Description

@tilucasoli

ColorToken cannot chain color operations (withValues, withOpacity)

Description

ColorRef (returned by calling a ColorToken) implements Color but throws UnimplementedError when you try to call color manipulation methods on it.

What throws:

$primary().withValues(alpha: 0.5)  // ❌ UnimplementedError at runtime
$primary().withOpacity(0.5)         // ❌ UnimplementedError at runtime

Steps to Reproduce

const $primary = ColorToken('brand.primary');

// In a widget build method:
final style = BoxStyler()
    .color($primary())                                  // ✅ Works
    .borderAll(color: $primary().withValues(alpha: 0.5));  // ❌ Throws at runtime!

Error message:

UnimplementedError: Cannot access 'withValues' on a Color token reference.

This is a context-dependent Color token that needs to be resolved through BuildContext before use.
Token references can only be passed directly to Mix styling utilities (e.g., $box.color).

To use as an actual Color value:
- Pass it to Mix utilities: $box.color.token(myColorToken)  
- Or resolve it first: myColorToken.resolve(context)

Current Workaround

Resolve the token first, then apply operations:

@override
Widget build(BuildContext context) {
  // Resolve first, then apply operations
  final primaryColor = $primary.resolve(context);
  
  final style = BoxStyler()
      .color(primaryColor)
      .borderAll(color: primaryColor.withValues(alpha: 0.5));
  
  return Box(style: style, child: child);
}

This works but:

  • Loses the ergonomic token syntax
  • Requires BuildContext at style definition time
  • Can't define styles outside of build methods

Expected Behavior

Color tokens should support chaining color operations that get applied when the token is resolved:

// Ideal API - operations stored as directives, applied at resolution
$primary.withValues(alpha: 0.5)   // Returns a modified token ref
$primary.withOpacity(0.5)          // Returns a modified token ref

// Usage in styles
final style = BoxStyler()
    .color($primary())
    .borderAll(color: $primary.withValues(alpha: 0.5));  // Should work!

Root Cause

In packages/mix/lib/src/theme/tokens/token_refs.dart:

final class ColorRef extends Prop<Color> with ValueRef<Color> implements Color {
  ColorRef(super.prop) : super.fromProp();
}

mixin ValueRef<T> {
  @override
  Never noSuchMethod(Invocation invocation) {
    throw UnimplementedError(_buildTokenReferenceError(invocation.memberName));
  }
}

ColorRef implements Color (so it can be passed where Color is expected) but delegates all unimplemented methods to noSuchMethod which throws.

Proposed Solutions

Option 1: Add directive methods to ColorToken

Add methods directly to ColorToken that return a new token reference with directives:

class ColorToken extends MixToken<Color> {
  const ColorToken(super.name);

  @override
  ColorRef call() => ColorRef(Prop.token(this));
  
  /// Returns a token ref with alpha applied when resolved
  ColorRef withValues({double? alpha, double? red, double? green, double? blue}) => ColorRef(
    Prop.directives([WithValuesColorDirective(alpha: alpha, red: red, green: green, blue: blue)], baseToken: this),
  );
  
  /// Returns a token ref with opacity applied when resolved  
  ColorRef withOpacity(double opacity) => ColorRef(
    Prop.directives([OpacityColorDirective(opacity)], baseToken: this),
  );
}

Option 2: Add methods to ColorRef

Make ColorRef return new ColorRef instances with accumulated directives:

final class ColorRef extends Prop<Color> with ValueRef<Color> implements Color {
  ColorRef(super.prop) : super.fromProp();
  
  @override
  ColorRef withValues({double? alpha, double? red, double? green, double? blue, ColorSpace? colorSpace}) => ColorRef(
    appendDirective(WithValuesColorDirective(alpha: alpha, red: red, green: green, blue: blue, colorSpace: colorSpace)),
  );
  
  @override
  ColorRef withOpacity(double opacity) => ColorRef(
    appendDirective(OpacityColorDirective(opacity)),
  );
}

Related Files

  • packages/mix/lib/src/theme/tokens/value_tokens.dart - ColorToken definition
  • packages/mix/lib/src/theme/tokens/token_refs.dart - ColorRef with noSuchMethod
  • packages/mix/lib/src/core/directive.dart - Color directives (WithValuesColorDirective, OpacityColorDirective)
  • packages/mix/lib/src/properties/painting/color_util.dart - ColorDirectiveMixin with existing directive support
  • examples/lib/api/context_variants/colortoken_operations_issue.dart - Reproduction example

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions