Flutter Animations: The Complete Guide With Code Examples

Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter animations guide, with AnimatedContainer, Tween curves, Hero transitions, and AnimationController visuals.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the complete Flutter animations guide.

Good animation is what separates an app that works from an app that feels alive. Flutter makes this unusually approachable: a whole family of Animated* widgets animate for you with a single property change, and when you need real control, the AnimationController and Tween system gives you frame-accurate command of the timeline. This guide covers both, from the one-line implicit animation to staggered choreography and custom page transitions, with runnable code throughout.

The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Every snippet is paste-ready against current stable Flutter. The golden rule before you start: reach for an implicit animation first. Most UI motion is a simple state-driven change, and the Animated* widgets handle those with no controller and no lifecycle to manage.

Follow me on Instagram@sagnikteaches

We will work up the ladder: implicit animations, then AnimatedSwitcher and Hero, then the explicit AnimationController and Tween system, then choreography (staggered animations), custom page-route transitions, and finally CustomPaint for drawing your own animated graphics. A short section on performance closes it out.

Connect on LinkedInSagnik Bhattacharya

This is one of three Flutter pillar guides published together. The Flutter Scrolling and Slivers guide and the Dart Language for Flutter guide cover the scrolling surfaces you will animate and the language every callback is written in.

Subscribe on YouTube@codingliquids

Implicit animations: the Animated* family

An implicit animation watches one or more properties and animates whenever they change. You give it a duration and a curve; Flutter does the in-between frames. The headline widget is AnimatedContainer — change its colour, size, padding, or alignment in setState and it smoothly transitions.

class _Box extends StatefulWidget {
  const _Box();
  @override
  State<_Box> createState() => _BoxState();
}

class _BoxState extends State<_Box> {
  bool _big = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _big = !_big),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 350),
        curve: Curves.easeInOut,
        width: _big ? 200 : 100,
        height: _big ? 200 : 100,
        decoration: BoxDecoration(
          color: _big ? Colors.deepPurple : Colors.amber,
          borderRadius: BorderRadius.circular(_big ? 32 : 8),
        ),
      ),
    );
  }
}

The family is large and you rarely need more than these:

  • AnimatedOpacity — fade a widget in and out by changing opacity.
  • AnimatedAlign / AnimatedPositioned — slide a child to a new position (the latter inside a Stack).
  • AnimatedPadding / AnimatedDefaultTextStyle — animate spacing and text style.
  • AnimatedCrossFade — cross-fade between exactly two children.
  • TweenAnimationBuilder — animate any value once, on first build, without writing a controller.

TweenAnimationBuilder deserves a special mention because it covers the gap between implicit and explicit: you give it a Tween and a builder, and it animates from begin to end whenever the end value changes.

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: 1),
  duration: const Duration(milliseconds: 600),
  curve: Curves.easeOutBack,
  builder: (context, value, child) => Transform.scale(
    scale: value,
    child: child,
  ),
  child: const FlutterLogo(size: 96),
)

AnimatedSwitcher: animate content swaps

When the content changes rather than a property — a number ticking up, an icon toggling — AnimatedSwitcher cross-fades the old child out and the new one in. Give each child a unique Key so the switcher knows they differ.

AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) =>
      ScaleTransition(scale: animation, child: child),
  child: Text(
    '$count',
    key: ValueKey<int>(count),
    style: const TextStyle(fontSize: 48),
  ),
)

Hero: shared-element transitions between screens

A Hero animation flies a widget from one screen to the next. Wrap the widget on both routes in a Hero with the same tag, and Flutter interpolates its position and size during navigation — no controller required.

// On the list screen
Hero(
  tag: 'avatar-$id',
  child: CircleAvatar(backgroundImage: NetworkImage(url)),
)

// On the detail screen — same tag
Hero(
  tag: 'avatar-$id',
  child: CircleAvatar(radius: 80, backgroundImage: NetworkImage(url)),
)

The tags must be unique within a screen and identical across the two routes. A duplicate tag on one screen throws, so derive the tag from a stable id.

The Complete Flutter Guide course thumbnail

Make your UI feel premium

The Complete Flutter Guide shows you how to weave animation into real screens without hurting performance.

Enrol now

Explicit animations: AnimationController and Tween

When you need to loop, reverse, or coordinate several animations from one timeline, you step up to an AnimationController. It produces a value from 0.0 to 1.0 over its duration, driven by a Ticker that fires once per frame. Because it needs a ticker, the State must mix in SingleTickerProviderStateMixin and pass vsync: this. Always dispose the controller.

class _Pulse extends StatefulWidget {
  const _Pulse();
  @override
  State<_Pulse> createState() => _PulseState();
}

class _PulseState extends State<_Pulse>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    )..repeat(reverse: true);

    _scale = Tween<double>(begin: 0.9, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scale,
      child: const Icon(Icons.favorite, color: Colors.red, size: 64),
    );
  }
}

Three ideas do all the work here. The controller owns the timeline (forward, reverse, repeat, stop). The Tween maps the 0–1 value onto the range you care about — a size, a colour, an offset. The CurvedAnimation applies easing so motion does not look robotic. The transition widgets — ScaleTransition, FadeTransition, SlideTransition, RotationTransition — rebuild efficiently without a setState on every frame.

AnimatedBuilder: animate anything

When no ready-made transition widget fits, AnimatedBuilder rebuilds just its builder each frame while keeping an expensive child out of the rebuild.

AnimatedBuilder(
  animation: _controller,
  child: const FlutterLogo(size: 80),
  builder: (context, child) => Transform.rotate(
    angle: _controller.value * 2 * 3.1415927,
    child: child,
  ),
)

Staggered animations: choreography from one controller

A staggered animation runs several tweens on one controller, each over a different slice of the timeline using Interval. The result is a sequence — fade in, then slide up, then scale — that stays perfectly in sync because it shares a single clock.

// Inside initState, with one _controller of duration 900ms
final _fade = CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.0, 0.4, curve: Curves.easeIn),
);
final _slide = Tween<Offset>(
  begin: const Offset(0, 0.3),
  end: Offset.zero,
).animate(CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.3, 0.7, curve: Curves.easeOut),
));
final _scale = Tween<double>(begin: 0.8, end: 1.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.6, 1.0, curve: Curves.easeOutBack),
  ),
);

// In build
FadeTransition(
  opacity: _fade,
  child: SlideTransition(
    position: _slide,
    child: ScaleTransition(scale: _scale, child: card),
  ),
)

For staggering a list of items — each row entering slightly after the previous — a small package such as flutter_staggered_animations wraps this pattern, but the principle above is all it does under the hood.

The Complete Flutter Guide course thumbnail

Build real animated screens

The Complete Flutter Guide takes these techniques into full apps — onboarding flows, transitions, and micro-interactions.

Enrol now

Custom page-route transitions

The default page transition is platform-appropriate, but you can supply your own with PageRouteBuilder. Its transitionsBuilder gives you the route's animation, which you drive into any transition widget — here, a slide combined with a fade.

Navigator.of(context).push(PageRouteBuilder(
  transitionDuration: const Duration(milliseconds: 400),
  pageBuilder: (context, animation, secondaryAnimation) => const DetailScreen(),
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    final curved = CurvedAnimation(parent: animation, curve: Curves.easeOutCubic);
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0, 0.15),
        end: Offset.zero,
      ).animate(curved),
      child: FadeTransition(opacity: curved, child: child),
    );
  },
));

CustomPaint: drawing your own animated graphics

When you need something the widget set cannot express — a progress arc, a waveform, a hand-drawn loader — CustomPaint with a CustomPainter lets you draw on a canvas, and feeding it an Animation via repaint animates it each frame.

class ArcPainter extends CustomPainter {
  ArcPainter(this.progress) : super(repaint: progress);
  final Animation<double> progress;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.deepPurple
      ..strokeWidth = 8
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
    final rect = Offset.zero & size;
    canvas.drawArc(rect, -1.57, 6.283 * progress.value, false, paint);
  }

  @override
  bool shouldRepaint(ArcPainter oldDelegate) => false;
}

Because the painter passes the animation to super(repaint:), Flutter repaints only the canvas each frame — not the surrounding widget tree.

Performance: keep animations at 60–120fps

  • Animate with transition widgets, not setState per frame. FadeTransition and friends, plus AnimatedBuilder, rebuild only what moves.
  • Pass a child to AnimatedBuilder so the expensive subtree is built once and reused every frame.
  • Always dispose controllers to stop their ticker and avoid leaks and "setState after dispose" errors.
  • Prefer Transform and opacity over animating layout properties; moving and fading are cheaper than re-laying-out.
  • Respect reduced-motion. Check MediaQuery.of(context).disableAnimations and shorten or skip non-essential motion.
  • Profile in profile mode with DevTools — see the Flutter Performance guide for the workflow.

Which animation tool should I use?

  • One property changing → AnimatedContainer / AnimatedOpacity and the rest of the implicit family.
  • Swapping content → AnimatedSwitcher.
  • Same element across screens → Hero.
  • Looping, reversing, or gesture-driven → AnimationController + Tween + a transition widget.
  • A sequence of timed steps → one controller with Interval curves (staggered).
  • A bespoke screen transition → PageRouteBuilder.
  • Custom drawn graphics → CustomPaint driven by an Animation.

Frequently asked questions

What is the difference between implicit and explicit animations?

Implicit animations (the Animated* widgets) animate automatically when a property changes — no controller. Explicit animations use an AnimationController you start, reverse, and repeat yourself, for loops, gestures, and choreography.

When do I need an AnimationController?

When you need to control timing directly — loop, reverse, coordinate several tweens, or drive motion from a gesture. The State must use SingleTickerProviderStateMixin and you must dispose the controller.

What is a Tween?

A Tween defines a begin and end value that an animation interpolates across as it moves from 0.0 to 1.0. Combine it with a controller via tween.animate(controller), usually wrapped in a CurvedAnimation for easing.

How do Hero animations work?

Wrap a widget on two screens in a Hero with the same tag. On navigation, Flutter flies the widget from its old position and size to the new one, creating a shared-element transition.

Further reads

Keep going with the tutorials that pair with this guide:

Sources: Flutter documentation — Introduction to animations, Implicit and Explicit animation tutorials, Hero animations, and Staggered animations (docs.flutter.dev); the animation and scheduler API references. Verified against current stable Flutter.