Flutter Color Animation with ColorTween

In Flutter, a color animation gradually interpolates between two colors over a specified duration. The ColorTween class defines the starting and ending colors, while an AnimationController controls when the transition runs.

This tutorial demonstrates two related behaviors: changing a button from indigo to lime once, and toggling the same color animation forward and backward whenever the button is pressed.

How ColorTween Produces a Flutter Color Transition

ColorTween(begin: ..., end: ...) calculates intermediate color values as the animation controller moves from 0.0 to 1.0. Calling animate(controller) connects the tween to the controller and returns an Animation<Color>.

  • begin is the color shown at the start of the animation.
  • end is the color shown when the animation completes.
  • duration determines how long the transition takes.
  • vsync prevents the controller from producing unnecessary frames when the widget is not visible.

The examples below are preserved in their original form. They use legacy null-safety syntax and the deprecated RaisedButton widget. In a current Flutter project, use nullable-safe declarations and a modern button such as ElevatedButton, or animate a Container directly.

Animate a Button Color from Indigo to Lime

The first example creates a one-second controller and starts it when the button is pressed. A listener calls setState() for each animation tick so that the button is rebuilt with the latest value from animation.value.

main.dart

</>
Copy
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Animation<Color> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    animation =
        ColorTween(begin: Colors.indigo, end: Colors.lime).animate(controller)
          ..addListener(() {
            setState(() {
              // The state that has changed here is the animation object’s value.
            });
          });
  }

  void animateColor() {
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Center(child: Text('Flutter - tutorialkart.com')),
      ),
      body: ListView(children: <Widget>[
        Container(
            margin: EdgeInsets.all(10),
            padding: EdgeInsets.all(20),
            height: 400,
            child: RaisedButton(
              onPressed: () => {animateColor()},
              color: animation.value,
              child: Text(''),
            ))
      ]),
    ));
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

When the application starts, the controller is at its initial value, so the button is indigo. Pressing the button calls controller.forward(), and the controller advances to its completed value while ColorTween supplies the intermediate colors. The final button color is lime.

Flutter animate color

Toggle the Flutter Color Animation Forward and Reverse

To return to the starting color, call controller.reverse(). The following version uses a Boolean flag to alternate between forward() and reverse() on successive button presses.

main.dart

</>
Copy
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Animation<Color> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 1), vsync: this);
    animation =
        ColorTween(begin: Colors.indigo, end: Colors.lime).animate(controller)
          ..addListener(() {
            setState(() {
              // The state that has changed here is the animation object’s value.
            });
          });
  }

  bool buttonToggle = true;

  void animateColor() {
    if (buttonToggle) {
      controller.forward();
    } else {
      controller.reverse();
    }
    buttonToggle = !buttonToggle;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      appBar: AppBar(
        title: Center(child: Text('Flutter - tutorialkart.com')),
      ),
      body: ListView(children: <Widget>[
        Container(
            margin: EdgeInsets.all(10),
            padding: EdgeInsets.all(20),
            height: 400,
            child: RaisedButton(
              onPressed: () => {animateColor()},
              color: animation.value,
              child: Text(''),
            ))
      ]),
    ));
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

On the first press, the controller moves from the beginning to the end of the animation. On the next press, it moves back to the beginning. Repeating this action toggles the button between indigo and lime.

Modern Flutter ColorTween Example with Null Safety

The following equivalent example uses current Dart null-safety syntax, late fields, AnimatedBuilder, and ElevatedButton. AnimatedBuilder rebuilds only the portion of the interface that depends on the animation, so a manual animation listener is not required.

</>
Copy
import 'package:flutter/material.dart';

void main() {
  runApp(const ColorAnimationApp());
}

class ColorAnimationApp extends StatefulWidget {
  const ColorAnimationApp({super.key});

  @override
  State<ColorAnimationApp> createState() => _ColorAnimationAppState();
}

class _ColorAnimationAppState extends State<ColorAnimationApp>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<Color?> _colorAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );

    _colorAnimation = ColorTween(
      begin: Colors.indigo,
      end: Colors.lime,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOut,
      ),
    );
  }

  void _toggleColor() {
    if (_controller.status == AnimationStatus.completed) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Color Animation'),
        ),
        body: Center(
          child: AnimatedBuilder(
            animation: _colorAnimation,
            builder: (context, child) {
              return SizedBox(
                width: 260,
                height: 260,
                child: ElevatedButton(
                  onPressed: _toggleColor,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: _colorAnimation.value,
                    foregroundColor: Colors.black,
                  ),
                  child: const Text('Toggle color'),
                ),
              );
            },
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Use AnimatedContainer for a Simpler Color Change

When you only need to animate a widget property after a state change, an implicit animation can be simpler. AnimatedContainer automatically animates changes to its color, size, padding, margin, alignment, and decoration. It does not require an AnimationController.

</>
Copy
class ColorBox extends StatefulWidget {
  const ColorBox({super.key});

  @override
  State<ColorBox> createState() => _ColorBoxState();
}

class _ColorBoxState extends State<ColorBox> {
  bool _isLime = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isLime = !_isLime;
        });
      },
      child: AnimatedContainer(
        duration: const Duration(seconds: 1),
        curve: Curves.easeInOut,
        width: 240,
        height: 240,
        color: _isLime ? Colors.lime : Colors.indigo,
        alignment: Alignment.center,
        child: const Text('Tap to change color'),
      ),
    );
  }
}

Choosing Between ColorTween and AnimatedContainer

RequirementRecommended approach
Start, stop, reverse, or repeat the animation manuallyColorTween with AnimationController
Animate a color automatically after a state value changesAnimatedContainer
Apply easing to a controller-driven color transitionWrap the controller in CurvedAnimation
Rebuild only the animated portion of the widget treeAnimatedBuilder

Common Flutter Color Animation Problems

  • The color does not change: confirm that the widget reads the current animation value during a rebuild.
  • The animation runs only once: after a controller completes, call reverse(), reset(), or repeat() before expecting another visible transition.
  • A ticker remains active: dispose of the AnimationController in the State object’s dispose() method.
  • The transition appears abrupt: increase the duration or use a curve such as Curves.easeInOut.
  • The code reports null-safety errors: use initialized or late controller and animation fields in current Dart code.

Flutter Color Animation FAQs

Can ColorTween animate transparent colors?

Yes. Set either endpoint to a transparent color, such as Colors.transparent. Make sure the surrounding widget and background produce the intended visual result while opacity changes.

How do I repeat a Flutter color animation?

Call controller.repeat(). To alternate continuously between the beginning and end colors, use controller.repeat(reverse: true).

How do I animate through more than two colors?

Use a TweenSequence<Color?> containing multiple TweenSequenceItem entries. Each item defines one segment of the full color animation.

Why must an AnimationController be disposed?

An animation controller owns a ticker that requests animation frames. Disposing the controller when the State object is removed releases that resource and prevents ticker-related warnings.

Flutter Color Animation Review Checklist

  • Verify that the starting and ending colors match the intended interface states.
  • Check that the animated widget rebuilds from the current animation value.
  • Confirm that forward, reverse, repeat, or reset behavior matches each user action.
  • Test text and icon contrast at intermediate as well as endpoint colors.
  • Ensure every manually created AnimationController is disposed.

Summary of Animating Colors in Flutter

Use ColorTween with an AnimationController when you need direct control over a color transition. Use controller.forward() to move toward the ending color and controller.reverse() to return to the starting color. For a simple state-driven transition, AnimatedContainer provides the same visual effect with less animation-management code.

In this Flutter Tutorial, we learned how to animate a color property, toggle a color animation, and choose between explicit and implicit animation approaches.