image

02 May 2026

9K

35K

Creating a Multi-Level Sidebar Menu Widget with Submenus, Animation, and Collapsible Sections in Flutter

Modern mobile and web applications often require sophisticated navigation solutions to provide a seamless user experience. A multi-level sidebar menu is a powerful tool for organizing complex application structures, allowing users to intuitively navigate through various sections and subsections. This article will guide you through building a dynamic, multi-level sidebar menu in Flutter, complete with submenus, smooth animations, and collapsible sections.

Understanding the Core Components

To achieve our goal, we'll break down the problem into several manageable parts:

  1. Data Model: A flexible structure to represent menu items and their children.
  2. Individual Menu Item Widget: A reusable widget capable of displaying a menu item, handling its expansion/collapse state, and displaying its children recursively.
  3. Animation: Smooth transitions for expanding and collapsing sections, enhancing user interaction.
  4. Main Sidebar Menu Widget: A container that takes a list of top-level menu items and renders them.

Step 1: Define the Menu Item Data Structure

First, let's create a simple data model to represent our menu items. Each item will have a title, an optional icon, and a list of children items. We'll also include a unique ID and a callback for when the item is tapped.


class MenuItem {
  final String id;
  final String title;
  final IconData? icon;
  final List<MenuItem> children;
  final VoidCallback? onTap;

  MenuItem({
    required this.id,
    required this.title,
    this.icon,
    this.children = const [],
    this.onTap,
  });
}

Step 2: Create the `SidebarMenuItem` Widget

This will be the most complex widget, responsible for rendering a single menu item. It needs to:

  • Display its own title and icon.
  • Show an expand/collapse icon if it has children.
  • Manage its own expanded state.
  • Animate the expansion/collapse of its children.
  • Recursively render its children using itself.

We'll use `StatefulWidget` to manage the expanded state, `AnimatedSize` for smooth height transitions, and `RotationTransition` for animating the expand/collapse arrow icon.


import 'package:flutter/material.dart';

// Assuming MenuItem class is defined as above

class SidebarMenuItem extends StatefulWidget {
  final MenuItem item;
  final int level;
  final ValueChanged<String> onItemSelected;

  const SidebarMenuItem({
    Key? key,
    required this.item,
    this.level = 0,
    required this.onItemSelected,
  }) : super(key: key);

  @override
  _SidebarMenuItemState createState() => _SidebarMenuItemState();
}

class _SidebarMenuItemState extends State<SidebarMenuItem> with SingleTickerProviderStateMixin {
  bool _isExpanded = false;
  late AnimationController _controller;
  late Animation<double> _iconTurns;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 200),
    );
    _iconTurns = Tween<double>(begin: 0.0, end: 0.5).animate(_controller);
  }

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

  void _handleTap() {
    if (widget.item.children.isNotEmpty) {
      setState(() {
        _isExpanded = !_isExpanded;
        if (_isExpanded) {
          _controller.forward();
        } else {
          _controller.reverse();
        }
      });
    } else {
      widget.onItemSelected(widget.item.id);
      widget.item.onTap?.call();
    }
  }

  @override
  Widget build(BuildContext context) {
    final bool hasChildren = widget.item.children.isNotEmpty;
    final double paddingLeft = 16.0 + (widget.level * 16.0); // Indentation for submenus

    return Column(
      children: [
        InkWell(
          onTap: _handleTap,
          child: Container(
            padding: EdgeInsets.only(left: paddingLeft, right: 16.0, top: 12.0, bottom: 12.0),
            child: Row(
              children: [
                if (widget.item.icon != null) ...[
                  Icon(widget.item.icon, size: 20.0),
                  const SizedBox(width: 12.0),
                ],
                Expanded(
                  child: Text(
                    widget.item.title,
                    style: TextStyle(
                      fontSize: 16.0 - (widget.level * 0.5), // Slightly smaller font for sub-levels
                      fontWeight: widget.level == 0 ? FontWeight.w500 : FontWeight.normal,
                    ),
                  ),
                ),
                if (hasChildren)
                  RotationTransition(
                    turns: _iconTurns,
                    child: const Icon(Icons.expand_more, size: 20.0),
                  ),
              ],
            ),
          ),
        ),
        if (hasChildren)
          AnimatedSize(
            duration: const Duration(milliseconds: 200),
            curve: Curves.easeInOut,
            child: SizedBox(
              height: _isExpanded ? null : 0.0, // Set height to null when expanded to allow content to dictate height
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: widget.item.children
                    .map((childItem) => SidebarMenuItem(
                          item: childItem,
                          level: widget.level + 1,
                          onItemSelected: widget.onItemSelected,
                        ))
                    .toList(),
              ),
            ),
          ),
      ],
    );
  }
}

Step 3: Create the `SidebarMenu` Widget (Main Container)

This widget will serve as the main container for our sidebar menu. It takes a list of top-level `MenuItem`s and renders them using the `SidebarMenuItem` widget.


import 'package:flutter/material.dart';

// Assuming MenuItem and SidebarMenuItem classes are defined above

class SidebarMenu extends StatelessWidget {
  final List<MenuItem> menuItems;
  final ValueChanged<String> onItemSelected;

  const SidebarMenu({
    Key? key,
    required this.menuItems,
    required this.onItemSelected,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: [
          const DrawerHeader(
            decoration: BoxDecoration(
              color: Colors.blue,
            ),
            child: Text(
              'App Navigation',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
              ),
            ),
          ),
          ...menuItems
              .map(
                (item) => SidebarMenuItem(
                  item: item,
                  onItemSelected: onItemSelected,
                ),
              )
              .toList(),
        ],
      ),
    );
  }
}

Step 4: Integrate into a `Scaffold`

Now, let's put it all together by integrating the `SidebarMenu` into a `Scaffold`'s `Drawer` and providing some sample data.


import 'package:flutter/material.dart';

// Assuming MenuItem, SidebarMenuItem, and SidebarMenu classes are defined above

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Multi-Level Sidebar Menu',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

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

class _MyHomePageState extends State<MyHomePage> {
  String _selectedItemId = 'dashboard';

  List<MenuItem> _buildMenuItems() {
    return [
      MenuItem(
        id: 'dashboard',
        title: 'Dashboard',
        icon: Icons.dashboard,
      ),
      MenuItem(
        id: 'products',
        title: 'Products',
        icon: Icons.shopping_bag,
        children: [
          MenuItem(
            id: 'all_products',
            title: 'All Products',
            icon: Icons.list,
          ),
          MenuItem(
            id: 'add_product',
            title: 'Add Product',
            icon: Icons.add_circle,
          ),
          MenuItem(
            id: 'categories',
            title: 'Categories',
            icon: Icons.category,
            children: [
              MenuItem(
                id: 'main_categories',
                title: 'Main Categories',
                icon: Icons.menu_book,
              ),
              MenuItem(
                id: 'sub_categories',
                title: 'Sub Categories',
                icon: Icons.subdirectory_arrow_right,
              ),
            ],
          ),
        ],
      ),
      MenuItem(
        id: 'orders',
        title: 'Orders',
        icon: Icons.receipt,
        children: [
          MenuItem(
            id: 'pending_orders',
            title: 'Pending Orders',
            icon: Icons.pending_actions,
          ),
          MenuItem(
            id: 'completed_orders',
            title: 'Completed Orders',
            icon: Icons.check_circle,
          ),
        ],
      ),
      MenuItem(
        id: 'settings',
        title: 'Settings',
        icon: Icons.settings,
      ),
    ];
  }

  void _onItemSelected(String id) {
    setState(() {
      _selectedItemId = id;
    });
    Navigator.pop(context); // Close the drawer
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Selected Item: $_selectedItemId')),
    );
    // In a real app, you would navigate to a new screen or update content here
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Multi-Level Sidebar Menu Demo'),
      ),
      drawer: SidebarMenu(
        menuItems: _buildMenuItems(),
        onItemSelected: _onItemSelected,
      ),
      body: Center(
        child: Text(
          'Current Selection: $_selectedItemId',
          style: const TextStyle(fontSize: 24),
        ),
      ),
    );
  }
}

Enhancements and Considerations

  • Selected Item Highlighting: To highlight the currently selected item, you could pass the `_selectedItemId` down to the `SidebarMenuItem` widgets and conditionally apply a background color or text style.
  • Deep Linking/Routing: For complex applications, integrate this menu with Flutter's Navigator 2.0 or a routing package (like GoRouter) to handle deep links and update the UI accordingly.
  • Customization: The styling (colors, fonts, padding) can be easily customized through the `ThemeData` or directly within the widgets.
  • Accessibility: Ensure proper semantic labels and focus management for users with accessibility needs.
  • Performance: For extremely large menus, consider optimizing the `ListView` or using `SliverList` if integrated directly into a custom scroll view.

Conclusion

You have now successfully built a dynamic, multi-level sidebar menu in Flutter, featuring submenus, smooth animations for expansion and collapse, and a clean hierarchical structure. This robust foundation can be adapted and expanded to suit the specific navigation needs of any Flutter application, significantly enhancing the user experience with its intuitive design and animated transitions.

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