image

08 Feb 2026

9K

35K

Flutter Animated Expandable Floating Action Button Menu

The Floating Action Button (FAB) is a prominent UI element in many modern applications, serving as the primary action for a screen. While a single FAB is intuitive, complex applications often require more than one key action. An expandable FAB menu elegantly solves this by revealing a set of related actions upon user interaction, all while maintaining a clean and uncluttered interface. Incorporating animations into this expansion not only enhances the user experience but also provides clear visual feedback, making the interaction feel fluid and natural.

The Power of Animation in UI

Animations are crucial for creating engaging and intuitive user interfaces. For an expandable FAB menu, animations serve several key purposes:

  • Visual Feedback: Users instantly understand that an action has been triggered and what the outcome is (e.g., the menu expanding).
  • Context and Flow: Animations guide the user's eye, making the transition between states smooth rather than abrupt, which can be disorienting.
  • Enhanced Aesthetics: A well-animated UI feels polished and professional, contributing to a premium user experience.
  • Reduced Cognitive Load: By showing how elements move and transform, animations help users build a mental model of the interface, making it easier to predict behavior.

Core Flutter Animation Concepts

To implement an animated expandable FAB, we'll leverage Flutter's powerful animation framework. Key components include:

  • AnimationController: Manages the animation's progress, duration, and state (e.g., forward, reverse, repeat).
  • Tween: Defines the range of values an animation should interpolate between (e.g., Tween<double>(begin: 0.0, end: 1.0)).
  • CurvedAnimation: Applies a non-linear curve to an animation, making it feel more natural (e.g., Curves.easeOutBack).
  • AnimatedBuilder: A widget that rebuilds its child when the animation's value changes, making it efficient for UI updates.
  • Transform.rotate and Transform.translate: Widgets for applying rotation and translation transformations, respectively.
  • Opacity: A widget to animate the transparency of its child.

Building the Animated Expandable FAB Menu

Let's walk through the steps to create a highly customizable animated expandable FAB menu.

1. Project Setup and Basic Structure

Start with a basic Flutter application. We'll create a StatefulWidget to manage the animation and menu state.


import 'package:flutter/material.dart';
import 'dart:math' as math; // For math.pi

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Animated FAB',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _rotateAnimation;
  late Animation<double> _scaleAnimation;
  late Animation<double> _translateAnimation;

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _rotateAnimation = Tween<double>(begin: 0.0, end: 0.75).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOut,
      ),
    );

    _scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOutBack,
      ),
    );

    _translateAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeOutBack,
      ),
    );
  }

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

  void _toggleMenu() {
    setState(() {
      _isExpanded = !_isExpanded;
      if (_isExpanded) {
        _animationController.forward();
      } else {
        _animationController.reverse();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated FAB Menu'),
      ),
      body: const Center(
        child: Text('Press the FAB to expand!'),
      ),
      floatingActionButton: _buildExpandableFab(),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
    );
  }

  // Helper methods to build the FABs will go here
  Widget _buildExpandableFab() {
    // Implementation details below
    return SizedBox.shrink(); // Placeholder
  }

  Widget _buildChildFab({
    required IconData icon,
    required VoidCallback onPressed,
    required double delay,
  }) {
    // Implementation details below
    return SizedBox.shrink(); // Placeholder
  }
}

2. The Expandable FAB Widget

The main FAB will toggle the expansion. Its icon (e.g., a plus sign) will rotate to indicate the expanded state (e.g., an 'X'). The child FABs will be positioned relative to the main FAB using a Stack.


  Widget _buildExpandableFab() {
    return Stack(
      alignment: Alignment.bottomRight,
      children: <Widget>[
        // Background overlay for when menu is open (optional)
        if (_isExpanded)
          Positioned.fill(
            child: GestureDetector(
              onTap: _toggleMenu, // Close menu when tapping outside
              child: Container(color: Colors.black.withOpacity(0.3)),
            ),
          ),
        _buildChildFab(
          icon: Icons.share,
          onPressed: () {
            _toggleMenu();
            // Handle share action
            ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Share pressed!')));
          },
          delay: 0.3, // Delay for staggered animation
        ),
        _buildChildFab(
          icon: Icons.edit,
          onPressed: () {
            _toggleMenu();
            // Handle edit action
            ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Edit pressed!')));
          },
          delay: 0.2,
        ),
        _buildChildFab(
          icon: Icons.add_photo_alternate,
          onPressed: () {
            _toggleMenu();
            // Handle photo action
            ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Photo pressed!')));
          },
          delay: 0.1,
        ),
        FloatingActionButton(
          heroTag: 'mainFab',
          onPressed: _toggleMenu,
          backgroundColor: Colors.blue,
          child: AnimatedBuilder(
            animation: _rotateAnimation,
            builder: (context, child) {
              return Transform.rotate(
                angle: _rotateAnimation.value * math.pi, // 180 degrees
                child: Icon(
                  _isExpanded ? Icons.close : Icons.add,
                ),
              );
            },
          ),
        ),
      ],
    );
  }

3. Animating Child FABs

Each child FAB will translate upwards and fade in/out. We'll use AnimatedBuilder to ensure these animations occur in sync with the main controller. Staggered delays create a more dynamic and visually appealing effect.


  Widget _buildChildFab({
    required IconData icon,
    required VoidCallback onPressed,
    required double delay,
  }) {
    // Create a delayed animation for each child FAB
    final Animation<double> delayedTranslateAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(
          0.0 + delay, // Start the animation after a delay
          1.0,
          curve: Curves.easeOutBack,
        ),
      ),
    );

    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        final double value = delayedTranslateAnimation.value;
        final double opacity = _isExpanded ? value : 0.0;
        final double translation = _isExpanded ? (value * 70.0 * (3 - delay * 10)) : 0.0;
        // The 70.0 * (3 - delay * 10) creates increasing distances for each FAB
        // For delay 0.1: 70 * 2 = 140
        // For delay 0.2: 70 * 1 = 70
        // For delay 0.3: 70 * 0 = 0 (this needs adjustment for proper stacking)
        // Let's refine the translation logic to be simpler for fixed distance.
        // Or better, let's use a fixed offset for each child.

        // Adjusted translation logic for fixed distances
        double distance = 70.0; // Distance between FABs
        double offset = 0.0;
        if (delay == 0.1) offset = distance * 3; // Furthest
        else if (delay == 0.2) offset = distance * 2; // Middle
        else if (delay == 0.3) offset = distance * 1; // Closest

        final double currentTranslation = _isExpanded ? (delayedTranslateAnimation.value * offset) : 0.0;

        return Positioned(
          right: 4.0, // Standard FAB padding
          bottom: currentTranslation + 80.0, // 80.0 is roughly height of main FAB + margin
          child: Opacity(
            opacity: opacity,
            child: Transform.scale(
              scale: _scaleAnimation.value, // Scale from 0 to 1
              child: FloatingActionButton(
                heroTag: 'childFab-${icon.codePoint}', // Unique heroTag for each FAB
                mini: true, // Make child FABs smaller
                onPressed: _isExpanded ? onPressed : null, // Disable onPressed when collapsed
                child: Icon(icon),
              ),
            ),
          ),
        );
      },
    );
  }

Explanation of Key Parts:

  • _MyHomePageState with SingleTickerProviderStateMixin: This mixin is required by AnimationController to synchronize animations with the screen refresh rate.
  • _animationController: Controls the overall animation duration. forward() and reverse() are called to open and close the menu.
  • _rotateAnimation: A Tween<double> that goes from 0.0 to 0.75 (representing 0 to 180 degrees when multiplied by math.pi). This animates the rotation of the main FAB's icon.
  • _scaleAnimation: A Tween<double> from 0.0 to 1.0, used to make child FABs scale up as they appear.
  • _translateAnimation (for individual child FABs): A `Tween` from 0.0 to 1.0, applied with an `Interval` to create staggered animations. This value is then multiplied by a fixed distance to determine the vertical position of each child FAB.
  • AnimatedBuilder: Wraps the main FAB's icon and each child FAB. It rebuilds only the parts of the widget tree dependent on the animation, making it efficient.
  • Stack and Positioned: Used to layer the main FAB and its children, allowing precise control over their placement. The bottom and right properties of Positioned are animated to create the expansion effect.
  • Opacity and Transform.scale: Used to animate the visibility and size of the child FABs, making their appearance and disappearance more dynamic.
  • heroTag: Crucial for FloatingActionButtons when used in a Stack or with multiple FABs, as it prevents animation errors. Each FAB must have a unique heroTag.
  • GestureDetector for overlay: When the menu is expanded, tapping the transparent overlay will close it, improving usability.

Conclusion

Creating an animated expandable Floating Action Button menu in Flutter significantly enhances the user experience by providing clear visual cues and a polished interface. By combining AnimationController, Tween, CurvedAnimation, and AnimatedBuilder with precise positioning using Stack and Positioned widgets, developers can craft intricate and delightful UI animations. This pattern is highly adaptable and can be customized with different animations, timings, and menu layouts to fit various application needs, making your Flutter app stand out.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is