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>.
beginis the color shown at the start of the animation.endis the color shown when the animation completes.durationdetermines how long the transition takes.vsyncprevents 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
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.

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
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.
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.
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
| Requirement | Recommended approach |
|---|---|
| Start, stop, reverse, or repeat the animation manually | ColorTween with AnimationController |
| Animate a color automatically after a state value changes | AnimatedContainer |
| Apply easing to a controller-driven color transition | Wrap the controller in CurvedAnimation |
| Rebuild only the animated portion of the widget tree | AnimatedBuilder |
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(), orrepeat()before expecting another visible transition. - A ticker remains active: dispose of the
AnimationControllerin the State object’sdispose()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
latecontroller 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
AnimationControlleris 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.
TutorialKart.com