Widget Modifiers
Prerequisites: This guide assumes familiarity with Mix’s Styling patterns. If you’re new to Mix, start there first.
Some visual effects — opacity, clipping, visibility — aren’t style properties like color or padding. They work by wrapping your widget in another Flutter widget (Opacity, ClipRRect, Visibility). In plain Flutter, you’d nest these manually. In Mix, modifiers let you declare them inside your style so they stay composable, mergeable, and animatable alongside everything else.
How Modifiers Work
Use .wrap() with a modifier to add a widget wrapper to your style:
final style = BoxStyler()
.color(Colors.red)
.size(100, 100)
.wrap(.opacity(0.4));This produces a widget tree equivalent to Opacity(opacity: 0.4, child: Container(...)).
Modifiers vs style properties
Style properties like .color() and .padding() are resolved into a single widget’s configuration. Modifiers add wrapper widgets around your styled widget. That’s why opacity needs .wrap() — it becomes an Opacity widget in the tree — while .color() is a direct property on the box.
Built-in Modifiers
| Modifier | Description | Wrapper Widget |
|---|---|---|
.opacity(value) | Sets opacity (0.0–1.0) | Opacity |
.padding(insets) | Adds padding | Padding |
.align(alignment) | Sets alignment | Align |
.aspectRatio(ratio) | Constrains aspect ratio | AspectRatio |
.flexible(flex: flex, fit: fit) | Makes flexible in Flex layouts | Flexible |
.transform(matrix) | Applies a raw transform matrix | Transform |
.visibility(visible: visible) | Shows/hides widget | Visibility |
.clipRect() | Clips to rectangle | ClipRect |
.clipRRect(borderRadius: radius) | Clips to rounded rectangle | ClipRRect |
.clipOval() | Clips to oval | ClipOval |
Combining Multiple Modifiers
Chain .wrap() calls to apply several modifiers:
final cardStyle = BoxStyler()
.color(Colors.white)
.size(200, 100)
.wrap(.opacity(0.9))
.wrap(.padding(.all(16)))
.wrap(.align(alignment: .center));Modifier Ordering
Default order
Mix does not apply modifiers in the order you chain them. Instead, it reorders them according to a built-in default pipeline that ensures correct visual results. The pipeline applies modifiers in six phases:
| Phase | Modifiers | Purpose |
|---|---|---|
| 1. Context & behavior | Flexible, Visibility, IconTheme, DefaultTextStyle | Establish layout participation and inherited context |
| 2. Size | SizedBox, FractionallySizedBox, IntrinsicHeight, IntrinsicWidth, AspectRatio | Set dimensions and constraints |
| 3. Layout | RotatedBox, Align | Position and rotate within layout space |
| 4. Spacing | Padding | Add spacing around content |
| 5. Visual transforms | Transform, Scale, Rotate, Translate, Skew, clips (ClipOval, ClipRRect, ClipPath, ClipRect, ClipTriangle) | Apply visual-only effects that don’t affect layout |
| 6. Final effects | Blur, Opacity, ShaderMask | Apply transparency and post-processing last |
This means you can chain modifiers in any order and get consistent results:
// These produce the same widget tree — Mix reorders them automatically
final a = BoxStyler()
.wrap(.opacity(0.5))
.wrap(.padding(.all(8)))
.wrap(.align(alignment: .center));
final b = BoxStyler()
.wrap(.align(alignment: .center))
.wrap(.padding(.all(8)))
.wrap(.opacity(0.5));
// Both result in:
// Align (phase 3)
// └─ Padding (phase 4)
// └─ Opacity (phase 6)
// └─ BoxCustom order
If the default pipeline doesn’t fit your use case, you can override it with .wrap(.orderOfModifiers(...)):
final style = BoxStyler()
.wrap(.opacity(0.5))
.wrap(.padding(.all(8)))
.wrap(.orderOfModifiers([
OpacityModifier, // Apply opacity first (innermost)
PaddingModifier, // Then padding (outermost)
]));Your custom order takes priority. Any modifiers not listed in your custom order fall back to their default position.
Global order via MixScope
To set a custom modifier order for your entire app (instead of per-style), pass orderOfModifiers to MixScope:
MixScope(
orderOfModifiers: [
VisibilityModifier,
PaddingModifier,
AlignModifier,
TransformModifier,
ClipRRectModifier,
OpacityModifier,
],
colors: { /* ... */ },
child: MyApp(),
);Every styled widget under this MixScope will use your custom order. Per-style overrides via .wrap(.orderOfModifiers(...)) still take precedence over the global setting.
When you override the order, the visual result changes. Opacity applied before padding means the padding area is also transparent. Opacity after padding means only the content is transparent.
Creating Custom Modifiers
Most use cases are covered by the built-in modifiers. If you need a modifier for a widget Mix doesn’t provide (e.g. a blur effect or a custom clip), you create two classes:
WidgetModifier— the resolved modifier that builds the wrapper widgetModifierMix— the unresolved mix that handles merging and token resolution
Step 1: WidgetModifier
The WidgetModifier holds the resolved values and builds the wrapper widget:
/// Modifier that applies opacity to its child.
final class OpacityModifier extends WidgetModifier<OpacityModifier> {
final double opacity;
const OpacityModifier([double? opacity]) : opacity = opacity ?? 1.0;
@override
OpacityModifier copyWith({double? opacity}) {
return OpacityModifier(opacity ?? this.opacity);
}
@override
OpacityModifier lerp(OpacityModifier? other, double t) {
if (other == null) return this;
return OpacityModifier(MixOps.lerp(opacity, other.opacity, t)!);
}
@override
List<Object?> get props => [opacity];
@override
Widget build(Widget child) {
return Opacity(opacity: opacity, child: child);
}
}| Method | Purpose |
|---|---|
build | Wraps the child widget with your Flutter widget |
lerp | Interpolates between two states for smooth animations |
copyWith | Creates a copy with optional property updates |
props | Properties used for equality comparison |
Step 2: ModifierMix
The ModifierMix holds unresolved Prop values (supporting tokens and directives) and knows how to merge and resolve them:
class OpacityModifierMix extends ModifierMix<OpacityModifier> {
final Prop<double>? opacity;
const OpacityModifierMix.create({this.opacity});
OpacityModifierMix({double? opacity})
: this.create(opacity: Prop.maybe(opacity));
@override
OpacityModifier resolve(BuildContext context) {
return OpacityModifier(MixOps.resolve(context, opacity));
}
@override
OpacityModifierMix merge(OpacityModifierMix? other) {
if (other == null) return this;
return OpacityModifierMix.create(
opacity: MixOps.merge(opacity, other.opacity),
);
}
@override
List<Object?> get props => [opacity];
}| Method | Purpose |
|---|---|
resolve | Resolves Prop values using BuildContext and creates the final WidgetModifier |
merge | Combines two instances — the other’s values take precedence |
props | Properties used for equality comparison |
Step 3: Use it
// Direct value
final style = BoxStyler()
.color(Colors.red)
.size(100, 100)
.wrap(OpacityModifierMix(opacity: 0.4));
// With a token
final $opacity = DoubleToken('custom.opacity');
final tokenStyle = BoxStyler()
.color(Colors.blue)
.wrap(OpacityModifierMix.create(opacity: Prop.token($opacity)));Best Practices
- Use modifiers for widget-level effects — opacity, visibility, clipping, and layout wrappers like
FlexibleandAlign - Use styler methods for style properties — color, padding, borders, shadows, and transforms (
.scale(),.rotate()) are style properties, not modifiers - Keep modifier chains short — more than 3-4 modifiers on a single style makes ordering hard to reason about. If you need many wrappers, consider splitting into separate styled widgets
- Implement
lerpfor animatable modifiers — without it, transitions between modifier states will snap instead of animating smoothly
See Also
- Styling — core Styler pattern and fluent chaining
- Animations — animate modifier transitions with implicit, phase, or keyframe animations
- Directives — transform values (text, numbers, colors) at resolve time