Animations & Motion Design

Implicit Animation Widgets: AnimatedOpacity, AnimatedAlign & AnimatedSwitcher

15 min Lesson 3 of 13

Implicit Animation Widgets: AnimatedOpacity, AnimatedAlign & AnimatedSwitcher

Flutter ships a family of implicit animation widgets that handle all animation logic internally. You simply set a new value, specify a duration and optional curve, and Flutter interpolates smoothly between the old and new values. In this lesson we focus on three powerful members of that family: AnimatedOpacity, AnimatedAlign, and AnimatedSwitcher.

AnimatedOpacity

AnimatedOpacity wraps any widget and animates its transparency between 0.0 (fully invisible) and 1.0 (fully opaque). It is the idiomatic way to fade widgets in and out without writing a single line of animation controller code.

  • opacity — the target opacity value; triggers the animation whenever it changes.
  • duration — how long the transition takes (e.g. Duration(milliseconds: 500)).
  • curve — easing curve; defaults to Curves.linear.
  • onEnd — optional callback fired when the animation completes.
Note: An invisible widget (opacity: 0.0) still occupies space in the layout. Use Visibility or conditional rendering if you also need to remove the widget from the layout.

AnimatedOpacity — Fade In/Out on Button Tap

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

  @override
  State<FadeDemo> createState() => _FadeDemoState();
}

class _FadeDemoState extends State<FadeDemo> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedOpacity(
          opacity: _opacity,
          duration: const Duration(milliseconds: 600),
          curve: Curves.easeInOut,
          child: Container(
            width: 200,
            height: 200,
            color: Colors.deepPurple,
            child: const Center(
              child: Text(
                'Flutter',
                style: TextStyle(color: Colors.white, fontSize: 28),
              ),
            ),
          ),
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _opacity = _opacity == 1.0 ? 0.0 : 1.0;
            });
          },
          child: const Text('Toggle Visibility'),
        ),
      ],
    );
  }
}

AnimatedAlign

AnimatedAlign animates its child’s position within the parent by interpolating an Alignment value. Common use-cases include sliding a panel in from an edge, highlighting an active tab indicator, or animating a floating action button to a new corner.

  • alignment — target Alignment; changing it starts the animation.
  • duration & curve — same semantics as all implicit widgets.
  • widthFactor / heightFactor — optional factors to size the widget relative to its child.
Tip: Combine AnimatedOpacity and AnimatedAlign around the same child to create a “slide-and-fade” entrance effect with zero boilerplate.

AnimatedAlign — Slide Between Corners

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

  @override
  State<AlignDemo> createState() => _AlignDemoState();
}

class _AlignDemoState extends State<AlignDemo> {
  Alignment _alignment = Alignment.topLeft;

  static const List<Alignment> _positions = [
    Alignment.topLeft,
    Alignment.topRight,
    Alignment.bottomRight,
    Alignment.bottomLeft,
  ];
  int _index = 0;

  void _next() {
    setState(() {
      _index = (_index + 1) % _positions.length;
      _alignment = _positions[_index];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SizedBox(
          width: 300,
          height: 300,
          child: AnimatedAlign(
            alignment: _alignment,
            duration: const Duration(milliseconds: 500),
            curve: Curves.easeInOutCubic,
            child: Container(
              width: 60,
              height: 60,
              decoration: const BoxDecoration(
                color: Colors.teal,
                shape: BoxShape.circle,
              ),
            ),
          ),
        ),
        ElevatedButton(
          onPressed: _next,
          child: const Text('Move'),
        ),
      ],
    );
  }
}

AnimatedSwitcher

AnimatedSwitcher cross-fades (or applies any custom transition) between two different child widgets. Whenever the child property is set to a widget with a different key, the old child plays an exit animation while the new child plays an entrance animation simultaneously.

  • duration — transition duration for the incoming child.
  • reverseDuration — optional separate duration for the outgoing child.
  • transitionBuilder — factory that wraps the child in a custom animated widget (default: FadeTransition).
  • layoutBuilder — controls how old and new children are stacked during the transition.
Warning: If the incoming and outgoing children have the same type and no explicit Key, Flutter considers them the same widget and does not trigger the transition. Always assign a ValueKey that changes with the content.

AnimatedSwitcher — Cross-Fade Between a Counter and an Icon

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

  @override
  State<SwitcherDemo> createState() => _SwitcherDemoState();
}

class _SwitcherDemoState extends State<SwitcherDemo> {
  int _count = 0;
  bool _showIcon = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 400),
          transitionBuilder: (child, animation) {
            return FadeTransition(
              opacity: animation,
              child: ScaleTransition(scale: animation, child: child),
            );
          },
          child: _showIcon
              ? const Icon(
                  Icons.check_circle,
                  key: ValueKey('icon'),
                  size: 80,
                  color: Colors.green,
                )
              : Text(
                  '$_count',
                  key: ValueKey(_count),
                  style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
                ),
        ),
        const SizedBox(height: 24),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => setState(() => _count++),
              child: const Text('Increment'),
            ),
            const SizedBox(width: 12),
            ElevatedButton(
              onPressed: () => setState(() => _showIcon = !_showIcon),
              child: const Text('Toggle View'),
            ),
          ],
        ),
      ],
    );
  }
}

Choosing the Right Widget

  • Use AnimatedOpacity when you want to fade a widget without moving it.
  • Use AnimatedAlign when you want to slide a widget to a new position within its parent.
  • Use AnimatedSwitcher when you want to swap one widget for another with a transition effect.

Summary

All three widgets follow the same implicit animation contract: set a new property value inside setState(), provide a duration, and Flutter does the rest. AnimatedOpacity controls transparency, AnimatedAlign controls alignment-based position, and AnimatedSwitcher transitions between entirely different child widgets. Mastering these three covers the vast majority of everyday UI motion needs without reaching for low-level AnimationController APIs.