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

Animations

Mix provides three main ways to animate your widgets, each suited to different levels of control and complexity:

  • Implicit: Simple property transitions that animate automatically when values change (state or variants).
  • Phase: Multi-step sequences that progress through defined phases, great for state machines.
  • Keyframe: Timeline-based animations with precise control over multiple properties and easing.

Implicit Animations

Implicit animations are the easiest way to animate style changes—Mix interpolates between old and new values automatically.

  • When to use: Simple hover/press effect or any other variant/state transition.
  • How it works: Call .animate(...) on any Style/Styler. Works with state and variants (e.g. hover, press, dark mode).

Example: State-triggered

This example demonstrates an implicit animation in Mix. When the appear state changes, the box smoothly transitions its scale between 0.1 and 1 using the .animate(...) modifier. The AnimationConfig.easeInOut(1.s) sets the animation to use an ease-in-out curve over 1 second.

class ScaleAnimation extends StatefulWidget { const ScaleAnimation({super.key}); @override State<ScaleAnimation> createState() => _ScaleAnimationState(); } class _ScaleAnimationState extends State<ScaleAnimation> { bool appear = false; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { appear = true; }); }); } @override Widget build(BuildContext context) { final style = BoxStyler() .color(Colors.black) .height(100) .width(100) .borderRounded(10) .scale(appear ? 1 : 0.1) // state-based .animate(AnimationConfig.easeInOut(1.s)); return Box(style: style); } }

Example: Variant-triggered

In the following example, the box smoothly grows in size and shifts its color from black to blue when you hover over it. Rather than toggling a state variable yourself, you can use variants like onHovered or onPressed to trigger these implicit animations directly in your style definitions.

class HoverAnimation extends StatelessWidget { const HoverAnimation({super.key}); @override Widget build(BuildContext context) { final style = BoxStyler() .color(Colors.black) .height(100) .width(100) .borderRounded(10) .scale(1) .onHovered( BoxStyler() .color(Colors.blue) .scale(1.5), ) .animate(AnimationConfig.spring(800.ms)); return Box(style: style); } }

Phase Animations

Phase animations let you define a small set of named phases, each with its own style and config. The sequence plays forward to completion, then resolves back to the initial phase.

  • When to use: State machines, progress indicators, or sequential micro-interactions that start with an initial value and, in the end, return to that same initial value.
  • How it works: Use .phaseAnimation(...) with:
    • trigger: a Listenable (e.g., ValueNotifier, ChangeNotifier).
    • phases: a list of values (usually an enum).
    • styleBuilder: returns the style for each phase.
    • configBuilder: returns the animation config for each phase.

Example: Compress/Expand

enum AnimationPhases { initial, compress, expanded } class CompressExpandAnimation extends StatefulWidget { const CompressExpandAnimation({super.key}); @override State<CompressExpandAnimation> createState() => _CompressExpandAnimationState(); } class _CompressExpandAnimationState extends State<CompressExpandAnimation> { final _isExpanded = ValueNotifier(false); @override void dispose() { _isExpanded.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final style = BoxStyler() .color(Colors.deepPurple) .height(100) .width(100) .borderRounded(40) .phaseAnimation( trigger: _isExpanded, phases: AnimationPhases.values, styleBuilder: (phase, style) => switch (phase) { AnimationPhases.initial => style.scale(1), AnimationPhases.compress => style.scale(0.75).color(Colors.red.shade800), AnimationPhases.expanded => style.scale(1.25).borderRounded(20).color(Colors.yellow.shade300), }, configBuilder: (phase) => switch (phase) { AnimationPhases.initial => CurveAnimationConfig.springWithDampingRatio(800.ms, ratio: 0.3), AnimationPhases.compress => CurveAnimationConfig.decelerate(200.ms), AnimationPhases.expanded => CurveAnimationConfig.decelerate(100.ms), }, ); return Pressable( onPress: () { _isExpanded.value = !_isExpanded.value; }, child: Box(style: style), ); } }

Keyframe Animations

Keyframes give you full control by mapping moments in time to values across multiple tracks (like CSS keyframes).

  • When to use: Choreographed sequences, multi-property transitions.
  • How it works: Use .keyframeAnimation(...) with a list of KeyframeTracks. Each track represents a property to animate and runs in parallel with the others, while the keyframes within a track are executed in sequence. Each keyframe defines a value, duration, and easing, and you can optionally provide a tweenBuilder for custom interpolation.

Example: Heart

class HeartKeyframeAnimation extends StatefulWidget { const HeartKeyframeAnimation({super.key}); @override State<HeartKeyframeAnimation> createState() => _HeartKeyframeAnimationState(); } class _HeartKeyframeAnimationState extends State<HeartKeyframeAnimation> { final _trigger = ValueNotifier(0); @override Widget build(BuildContext context) { final style = IconStyler() .color(Colors.red) .size(80) .keyframeAnimation( trigger: _trigger, timeline: [ KeyframeTrack<Color>( 'color', [ Keyframe.linear(Colors.blue.shade100, 100.ms), Keyframe.elasticOut(Colors.blue.shade400, 800.ms), Keyframe.elasticOut(Colors.green.shade100, 800.ms), ], initial: Colors.red.shade100, tweenBuilder: ColorTween.new, ), KeyframeTrack<double>('scale', [ Keyframe.linear(1.0, 360.ms), Keyframe.elasticOut(1.5, 800.ms), Keyframe.elasticOut(1.0, 800.ms), ], initial: 1.0), KeyframeTrack<double>('verticalOffset', [ Keyframe.linear(0.0, 100.ms), Keyframe.easeIn(20.0, 150.ms), Keyframe.elasticOut(-60.0, 1000.ms), Keyframe.elasticOut(0.0, 800.ms), ], initial: 0.0), KeyframeTrack<double>('verticalStretch', [ Keyframe.ease(1.0, 100.ms), Keyframe.ease(0.6, 150.ms), Keyframe.ease(1.5, 100.ms), Keyframe.ease(1.05, 150.ms), Keyframe.ease(1.0, 880.ms), Keyframe.ease(0.8, 100.ms), Keyframe.ease(1.04, 400.ms), Keyframe.ease(1.0, 220.ms), ], initial: 1.0), KeyframeTrack<double>('angle', [ Keyframe.easeIn(0.0, 580.ms), Keyframe.easeIn(16.0 * (pi / 180), 125.ms), Keyframe.easeIn(-16.0 * (pi / 180), 125.ms), Keyframe.easeIn(16.0 * (pi / 180), 125.ms), Keyframe.easeIn(0.0, 125.ms), ], initial: 0.0), ], styleBuilder: (values, style) { final scale = values.get('scale'); final verticalOffset = values.get('verticalOffset'); final verticalStretch = values.get('verticalStretch'); final angle = values.get('angle'); return style.wrapTransform( Matrix4.identity() ..scaleByDouble(scale, scale, scale, 1.0) ..translateByDouble(0, verticalOffset, 0, 1) ..scaleByDouble(1, verticalStretch, 1, 1) ..rotateZ(angle), ); }, ); return Pressable( onPress: () { _trigger.value++; }, child: StyledIcon(icon: CupertinoIcons.heart_fill, style: style), ); } }

Comparison

TypeUse CaseComplexityControl
ImplicitSimple state transitionsLowBasic
PhaseMulti-step/state machineMediumModerate
KeyframeChoreographed, multi-trackHighFull

Notes

  • Animation triggers: Phase and keyframe animations use a Listenable to start/advance animations.
  • Configs: Use AnimationConfig for timing, easing and delay.
  • Works with context: Variants like hover/press/dark mode combine well with animations.