Flutter package codecov Pub Points Deploy demo website License

collapsible_side_menu

collapsible_side_menu is a highly customizable Flutter package for building a collapsible side menu, with text direction (LTR & RTL) and sub-menu features.

Mobile Desktop Web
Mobile Desktop Web

Mobile and desktop demo videos; or look here for live demo.

Table of contents

Features

  • set menu animation duration
  • set menu behaviour
  • set menu text direction
  • set toggle button and it's look
  • add tiles and and their sub-tiles
  • customize menu, tile and sub-tile look
  • adaptive header widget (addon)

https://github.com/user-attachments/assets/964fe34e-0e02-425f-96d1-25a96e559a36

Usage

Installation

Add to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  # add this line
  collapsible_side_menu: latest_version

or run

flutter pub add collapsible_side_menu

Import package

Add the following line to your code

import 'package:collapsible_side_menu/collapsible_side_menu.dart';

Basic usage example

static const TextDirection menutextDirection = TextDirection.rtl;

CollapsibleSideMenu(
  defaultIndex: 3,
  defaultBehaviour: .open,
  menuStyle: SideMenuStyle(textDirection: menutextDirection),
  toggleButtonStyle: const ToggleButtonStyle(topPosition: 55, iconSize: 16),
  header: (_, isOpen) {
    return Column(
      crossAxisAlignment: .start,
      children: [
        Row(
          spacing: 10,
          mainAxisSize: .min,
          children: [
            const Flexible(child: CircleAvatar(radius: 22, child: FlutterLogo())),
            if (isOpen)
              const Flexible(
                flex: 2,
                child: Column(
                  crossAxisAlignment: .start,
                  children: [
                    Text(
                      'Joanna Doe',
                      overflow: .ellipsis,
                      style: TextStyle(fontWeight: .w500, fontSize: 14.5),
                    ),
                    Text(
                      'joanna.doe.404@flutter.dev',
                      overflow: .ellipsis,
                      style: TextStyle(fontSize: 12, fontWeight: .w300),
                    ),
                  ],
                ),
              ),
          ],
        ),
        const Divider(thickness: .5),
      ],
    );
  },
  footer: (_, isOpen) {
    return Column(
      mainAxisAlignment: .end,
      crossAxisAlignment: .start,
      children: [
        const Divider(thickness: .5),
        Text('v1.0.1+1', style: TextStyle(fontSize: isOpen ? 12 : 10)),
      ],
    );
  },
  items: [
    TileData(
      title: 'User management',
      leading: const Icon(Icons.person_outline, size: 18),
      selectedLeading: const Icon(Icons.person, size: 18),
      subTiles: [
        SubTileData(title: 'Customers'),
        SubTileData(
          title: 'Employees',
          subTiles: [
            SubTileData(title: 'Drivers'),
            SubTileData(title: 'HR'),
            SubTileData(title: 'Accountants'),
            SubTileData(title: 'Marketing'),
          ],
        ),
        SubTileData(
          title: 'Admins',
          subTiles: [
            SubTileData(
              onTap: () {
                ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Sudo')));
              },
              title: 'Sudo',
              leading: const Icon(Icons.shield, size: 18),
            ),
            SubTileData(title: 'Super-admins'),
            SubTileData(title: 'Admins'),
          ],
        ),
        SubTileData(title: 'Salaries', trailing: const Icon(Icons.attach_money, size: 18)),
      ],
    ),
    //
    const DividerData(),
    const TitleData(title: 'OTHERS'),
    TileData(onTap: () {}, title: 'Vehicle', leading: const Icon(Icons.car_rental, size: 18)),
    TileData(
      onTap: () {},
      title: 'Conversations',
      leading: const Icon(Icons.chat_bubble_outline, size: 18),
      selectedLeading: const Icon(Icons.chat_bubble, size: 18),
      /// use whatever badge package ou UI you want by wrapping tile with it
      badgeBuilder: (tile) => Badge.count(count: 100, maxCount: 9, offset: Offset(menutextDirection == .rtl ? 2 : -2, -4), child: tile),
    ),
    TileData(title: 'Labels', leading: const Icon(Icons.label_outline), selectedLeading: const Icon(Icons.label)),
    TileData(
      onTap: () {
        showAdaptiveAboutDialog(context: context);
      },
      title: 'About',
      leading: const Icon(Icons.info_outline, size: 18),
      selectedLeading: const Icon(Icons.info, size: 18),
    ),
    TileData(
      onTap: () {
        showLicensePage(context: context);
      },
      title: 'Licenses',
      leading: const Icon(Icons.copyright_outlined, size: 18),
      selectedLeading: const Icon(Icons.copyright, size: 18),
    ),
  ],
  onIndexChanged: (index) {
    debugPrint('current index: $index');
  },
),

🚩 BREAKING CHANGES from 1.x.x to 2.x.x

NEW Type Description
SideMenuHeader Widget Menu header widget. Auto adapts to menu state changes (Addon).
SideMenuController().onCollapsedChanged void Function(bool)
CollapsibleSideMenu().of(context) CollapsibleSideMenu To access CollapsibleSideMenu members from descendants
CollapsibleSideMenu().maybeOf(context) CollapsibleSideMenu? To access CollapsibleSideMenu members from descendants
TileData().tooltip String? To override default tooltip when menu is collapsed
REMOVED Type
SideMenuController().isCollapsed bool Function
TileData().id String?
SubTileData().id String?

Essentials

Elements, types, usage and description

Element Type Usage Description
SideMenu Widget Menu widget The side menu widget
SideMenuStyle Class Style Menu container style
MenuTileStyle Class Style Menu tile style
SubMenuTileStyle Class Style Menu sub-tile style
ToggleButtonStyle Class Style Toggle button style (to open/close the menu)
SideMenuController Class Controller Menu controller
TitleData Class Data Add a simple text with custom style (with no background or tap callback) to items
DividerData Class Data Add a custom divider widget to items
TileData Class Data Add a tile to items
SubTileData Class Data Add a sub-tile to tile (TileData)
TileBadgeBuilder Class Builder Data to build a badge on a tile
CustomChildPosition Enum Enum Position of the custom child related to items (above or below)
AutoOpenFrom Enum Enum Set from what screen width the menu should adapt when defaultBehaviour is set to auto

Class, parameters, types and defaults

Class Parameters Types Defaults
SideMenu header MenuHeaderBuilder? null
footer MenuHeaderBuilder? null
customMenuChild CustomMenuChild? null
items List<SideMenuItem> const []
spacerAfterItems Spacer? null
onIndexChanged ValueChanged<int>? null
controller SideMenuController? null
defaultBehaviour MenuBehaviour MenuBehaviour.auto
autoFrom AutoOpenFrom AutoOpenFrom.tablet
minWidth double MenuConstants.minWidth
maxWidth double MenuConstants.maxWidth
hasToggleButton bool true
toggleButtonStyle ToggleButtonStyle? null
duration Duration MenuConstants.duration
animationCurve Curve Curves.linear
menuStyle SideMenuStyle? null
defaultIndex int? null
SideMenuStyle backgroundColor Color? colorScheme.primary
boxShadow BoxShadow? null
textDirection TextDirection? Directionality.of(context)
borderRadius BorderRadiusGeometry MenuConstants.borderRadius
padding EdgeInsetsGeometry MenuConstants.menuInnerPadding
margin EdgeInsetsGeometry MenuConstants.menuOuterPadding
defaultTileStyle MenuTileStyle null
MenuTileStyle titleStyle TextStyle? TextStyle(fontSize: 13.7)
selectedTitleStyle TextStyle? TextStyle(fontSize: 13.7, fontWeight: .w500)
color Color? Set depending on menu background color luminance (if not null), else set to colorScheme.onPrimary
selectedColor Color? It's set to Colors.black or Colors.white depending on colorScheme.inversePrimary luminance
hoverColor Color? colorScheme.onSecondaryContainer
backgroundColor Color MenuConstants.transparent
selectedBackgroundColor Color? colorScheme.inversePrimary
borderRadius BorderRadius MenuConstants.borderRadius
decoration Decoration? BoxDecoration(borderRadius: MenuConstants.borderRadius)
selectedDecoration Decoration? BoxDecoration(color: colorScheme.inversePrimary, borderRadius: MenuConstants.borderRadius)
selectedIndicator Decoration? BoxDecoration(borderRadius: MenuConstants.borderRadius) with the right color
selectedBorderWidth double MenuConstants.selectedBorderWidth
padding EdgeInsetsGeometry EdgeInsets.zero
margin EdgeInsetsGeometry MenuConstants.tileMargin
subTileStyle SubMenuTileStyle? refer to SubMenuTileStyle section
tileHeight double MenuConstants.tileHeight
subTileHeight double MenuConstants.subTileHeight
horizontalSpacing double MenuConstants.horizontalSpacing
leadingIconSize double? null
trailingIconSize double? null
SubMenuTileStyle titleStyle TextStyle? TextStyle(fontSize: 12.3)
selectedTitleStyle TextStyle? TextStyle(fontSize: 12.3, fontWeight: .w500)
selectedColor Color? colorScheme.onSecondaryContainer
backgroundColor Color MenuConstants.transparent
color Color? Set depending on background color luminance (if not null), else set to colorScheme.onSecondary
hoverColor Color? colorScheme.onSecondaryContainer
selectedBackgroundColor Color? null
borderRadius BorderRadius MenuConstants.borderRadius
decoration Decoration? BoxDecoration(borderRadius: MenuConstants.borderRadius)
selectedDecoration Decoration? BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: MenuConstants.borderRadius)
selectedBorderWidth double MenuConstants.selectedBorderWidth
padding EdgeInsetsGeometry EdgeInsets.zero
margin EdgeInsetsGeometry MenuConstants.tileMargin
horizontalSpacing double MenuConstants.horizontalSpacing
defaultSubTilesStyle SubMenuTileStyle? null
tileHeight double MenuConstants.subTileHeight
leadingIconSize double? null
trailingIconSize double? null
ToggleButtonStyle iconColor Color? null
topPosition double 20.0
opacity double 0.7
iconSize double 20.0
icon IconData? null
backgroundColor Color? ColorScheme.of(context).inversePrimary

⚠️ Notice

This package was only tested on Android, Linux and web so feedback is needed for other platforms.