Skip to Content
Mix 2.0 is in development! You can access the Mix 1.0 docs here.
DocsGuidesAnimations

Animations

Mix gives you three ways to bring motion into your UI, depending on how much control you want:

Implicit — Easiest to use. When a value changes (state or a variant like hover), the style animates smoothly to the new value. No timeline to manage.

Phase — For multi-step flows that go through distinct steps and then back (e.g. tap → compress → expand → rest). Think of it as a small state machine for animation.

Keyframe — Full control. You define a timeline with tracks and keyframes, so multiple properties can change over time with precise easing.


Implicit Animations

When something in your UI changes—a color on hover, a size on tap, or switching to dark mode—you often want that change to feel smooth rather than instant. Implicit animations do exactly that: whenever a value flips from A to B, Mix interpolates between them so the transition feels natural.

How to use: Add .animate(AnimationConfig....) to any Style or Styler. It works with regular state (e.g. setState) and with variants like onHovered, onPressed, and onDark.

Case 1: State-triggered

In this example a square grows each time you tap it.

int _counter = 2; final box = BoxStyler() .color(Colors.deepPurple) .size(_counter * 10, _counter * 10) .animate(.spring(1.s, bounce: 0.6)); return Pressable( onPress: () => setState(() => _counter += 3), child: box(), );
Resolving preview metadata...

Case 2: Variant-triggered

Animations also work with variants. When the widget enters a variant (e.g. hovered), the style animates toward the variant’s “target” style; .animate(...) controls how that transition runs.

final box = BoxStyler() .color(Colors.black) .size(100, 100) .borderRounded(10) .scale(1) .onHovered(.color(Colors.blue).scale(1.5)) .animate(.spring(800.ms)); return box();
Resolving preview metadata...

Phase Animations

Sometimes you need more than a simple A→B transition: a button that squashes on press then bounces back, or a flow that goes through several distinct steps and returns to the start. Phase animations are built for that. You define a set of phases, and for each one you choose the style and how the transition into that phase should animate.

How to use: Use .phaseAnimation(...) to define your phases and what style and animation config each one uses. Optionally pass a trigger—with it, the animation runs only when that value changes; without it, it loops on its own.

Case: Tap → compress → expand → initial

Below, a square reacts to tap: it compresses, then expands, then returns to its original size, moving through each phase in order.

enum AnimationPhases { initial, compress, expanded } final _isExpanded = ValueNotifier(false); final box = BoxStyler() .color(Colors.deepPurple) .height(100) .width(100) .borderRounded(40) .phaseAnimation( trigger: _isExpanded, phases: AnimationPhases.values, styleBuilder: (phase, style) => switch (phase) { .initial => style.scale(1), .compress => style.scale(0.75).color(Colors.red.shade800), .expanded => style.scale(1.25).borderRounded(20).color(Colors.yellow.shade300), }, configBuilder: (phase) => switch (phase) { .initial => .springWithDampingRatio(800.ms, ratio: 0.3), .compress => .decelerate(200.ms), .expanded => .decelerate(100.ms), }, ); return Pressable( onPress: () => _isExpanded.value = !_isExpanded.value, child: box(), );
Resolving preview metadata...

Keyframe Animations

When you want full control—a thumb that scales and slides, a pop-in that combines scale, opacity, and offset, or any timeline where several values change in sync—keyframe animations are the tool. You build a timeline with tracks and keyframes, and Mix drives your style from those values.

How to use: With .keyframeAnimation(...) you define a timeline of tracks. Each track has keyframes that run in sequence; tracks run in parallel. A style builder then maps the current track values onto your style. Optionally pass a trigger so the animation runs only when that value changes; without it, the animation loops on its own.

Case 1: Simple toggle (scale + width)

A single trigger drives two tracks (scale and width). The style builder reads both values and applies them together. Note that each track has its own keyframe with its own values and duration.

final _trigger = ValueNotifier(false); final box = BoxStyler() .height(30) .width(40) .color(Colors.deepPurpleAccent) .shapeStadium() .keyframeAnimation( trigger: _trigger, timeline: [ KeyframeTrack('scale', [ .easeOutSine(1.25, 200.ms), .elasticOut(0.85, 500.ms), ], initial: 0.85), KeyframeTrack<double>('width', [ .decelerate(50, 100.ms), .ease(80, 300.ms), .elasticOut(40, 500.ms), ], initial: 40), ], styleBuilder: (values, style) => style.scale(values.get('scale')).width(values.get('width')), ); return Center( child: Pressable( onPress: () { setState(() { _trigger.value = !_trigger.value; }); }, child: box(), ), );
Resolving preview metadata...

Case 2: Loop animation (scale + color + opacity)

Several tracks run in parallel to create one looping animation. The style builder applies scale, color, and opacity from the tracks. Here we use a ColorTween to interpolate between the initial and end colors.

The Keyframe animation feature allows you to use any custom Tween you want to interpolate between the initial and end values.

final box = BoxStyler() .size(60, 60) .alignment(.centerLeft) .keyframeAnimation( timeline: [ KeyframeTrack('scale', [ .springWithBounce(1.0, 2000.ms, bounce: 0.5), ], initial: 0.0), KeyframeTrack<Color>( 'color', [.easeInOut(Colors.deepPurpleAccent, 2000.ms)], initial: Colors.grey.shade300, tweenBuilder: ColorTween.new, ), KeyframeTrack('opacity', [.easeIn(1.0, 500.ms)], initial: 0.0), ], styleBuilder: (values, style) { final scale = values.get('scale'); final opacity = values.get('opacity'); return style .transform(Matrix4.diagonal3Values(scale, scale, 1.0)) .color(values.get('color')) .wrap(WidgetModifierConfig.opacity(opacity)); }, ); return box();
Resolving preview metadata...

For a more advanced keyframe example (e.g. multi-track heart with color, scale, offset, stretch, rotation), see the keyframe heart example in the examples app.


Conclusion

Start with implicit when you just want values to transition smoothly. Move to phase when you have a clear sequence of steps that return to the start. Reach for keyframe when you need a full timeline with multiple properties and precise control.