Skip to Content
DocsGuidesWidget Modifiers

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));
Resolving preview metadata...

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

ModifierDescriptionWrapper Widget
.opacity(value)Sets opacity (0.0–1.0)Opacity
.padding(insets)Adds paddingPadding
.align(alignment)Sets alignmentAlign
.aspectRatio(ratio)Constrains aspect ratioAspectRatio
.flexible(flex: flex, fit: fit)Makes flexible in Flex layoutsFlexible
.transform(matrix)Applies a raw transform matrixTransform
.visibility(visible: visible)Shows/hides widgetVisibility
.clipRect()Clips to rectangleClipRect
.clipRRect(borderRadius: radius)Clips to rounded rectangleClipRRect
.clipOval()Clips to ovalClipOval

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:

PhaseModifiersPurpose
1. Context & behaviorFlexible, Visibility, IconTheme, DefaultTextStyleEstablish layout participation and inherited context
2. SizeSizedBox, FractionallySizedBox, IntrinsicHeight, IntrinsicWidth, AspectRatioSet dimensions and constraints
3. LayoutRotatedBox, AlignPosition and rotate within layout space
4. SpacingPaddingAdd spacing around content
5. Visual transformsTransform, Scale, Rotate, Translate, Skew, clips (ClipOval, ClipRRect, ClipPath, ClipRect, ClipTriangle)Apply visual-only effects that don’t affect layout
6. Final effectsBlur, Opacity, ShaderMaskApply 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) // └─ Box

Custom 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:

  1. WidgetModifier — the resolved modifier that builds the wrapper widget
  2. ModifierMix — 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); } }
MethodPurpose
buildWraps the child widget with your Flutter widget
lerpInterpolates between two states for smooth animations
copyWithCreates a copy with optional property updates
propsProperties 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]; }
MethodPurpose
resolveResolves Prop values using BuildContext and creates the final WidgetModifier
mergeCombines two instances — the other’s values take precedence
propsProperties 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 Flexible and Align
  • 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 lerp for 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