Building a Tree-Based Sidebar Menu in Flutter Web

Abraham Aditya
Bina Nusantara IT Division
6 min readOct 3, 2023

The sidebar menu is one of the commonly used navigation elements in web design. Its purpose is to allow users to easily navigate through various pages or features, centralized in one area located on the side of the screen. Typically, the sidebar menu contains links, text, or icons that facilitate user access to various sections of the website. Its presence is crucial in enhancing the user experience as it assists users in navigating and finding the information or features they need. Therefore, it’s important to carefully design and organize the sidebar menu to ensure accessibility and intuitive usage by website visitors. As the name suggests, the sidebar menu is positioned vertically on the side of the screen.

Within the Flutter framework, there is a feature similar to the sidebar menu known as the Navigation Drawer. This feature is commonly used in mobile app development with Flutter. However, when working with Flutter for web, we can leverage publicly available packages on https://pub.dev/packages?q=sidebar, such as sidebarx, collapsible_sidebar, and sidebar_drawer.

However, there are limitations in using these packages, especially when we want to create a sidebar menu with a tree-based concept. This concept enables the creation of a more dynamic and comprehensive sidebar menu and provides a hierarchical tree-like data structure that can be visualized as a linear list view, similar to a Tree view that makes it easier for users to explore various list options. You will often see this concept being used in menus on documentation pages containing articles or in admin and dashboard pages.

In this instance, we will create a custom sidebar menu with a tree-based structure concept using the Tree-View from the package owned by CubiVue, known as animated_tree_view.

Here are the results of implementing the creation of the sidebar tree menu:

Step 1: Creating the Tree

Beforehand, we need to add the animated_tree_view package with the following command:

  • Run this command in the terminal.
$ flutter pub add animated_tree_view
  • The system will automatically add a line like this to the pubspec.yaml file.
dependencies:
animated_tree_view: ^2.1.0
  • Import it into the Dart code file.
import 'package:animated_tree_view/animated_tree_view.dart';
  • Create a variable named <menuTree> as the tree menu structure that will be displayed in the sidebar menu.
final menuTree = TreeNode.root()
..addAll(
[
TreeNode(key: "Dashboard", data: Icons.dashboard),
TreeNode(key: "Documentation", data: Icons.description)
..addAll([
TreeNode(key: "Dart"),
TreeNode(key: "Flutter"),
]),
TreeNode(key: "Plugins", data: Icons.cable)
..addAll([
TreeNode(key: "Animated Tree View"),
TreeNode(key: "Flutter BLoC"),
TreeNode(key: "Material"),
]),
TreeNode(key: "Analytics", data: Icons.analytics),
TreeNode(key: "Collection", data: Icons.collections_bookmark)
..addAll([
TreeNode(key: "Framework"),
TreeNode(key: "Technology"),
]),
TreeNode(key: "Settings", data: Icons.settings),
],
);

Step 2: Creating the BLoC

Since this sidebar menu is stateful, we need to use state management to operate its features. For this example, BLoC will be employed as the state management solution. In the flow of the process, an event will receive a menu key string that the user clicks, and then this string key will be emitted back to be built by BLoCBuilder on the front page according to the page the user wants to navigate to.

We need to create 3 component files:

  • BLoC
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sidebar_menu/bloc/sidebar_menu_bloc/sidebar_menu_event.dart';
import 'package:sidebar_menu/bloc/sidebar_menu_bloc/sidebar_menu_state.dart';

class SidebarMenuBloc extends Bloc<SidebarMenuEvent, SidebarMenuState> {
SidebarMenuBloc() : super(SidebarMenuInitial()) {
on<FetchSidebarMenuEvent>((_, emit) async {
try {
emit(SidebarMenuSuccess(_.menu!));
} catch (e) {
emit(
SidebarMenuError(e.toString()),
);
}
});
}
}
  • Event
import 'package:equatable/equatable.dart';

abstract class SidebarMenuEvent extends Equatable {}

class InitEvent extends SidebarMenuEvent {
InitEvent();
@override
List<Object?> get props => [];
}

class FetchSidebarMenuEvent extends SidebarMenuEvent {
final String? menu;
FetchSidebarMenuEvent({required this.menu});
@override
List<Object?> get props => [];
}
  • State
import 'package:equatable/equatable.dart';

abstract class SidebarMenuState extends Equatable {
const SidebarMenuState();
@override
List<Object> get props => [];
get temp => null;
}

class SidebarMenuInitial extends SidebarMenuState {}

class SidebarMenuLoading extends SidebarMenuState {}

class SidebarMenuSuccess extends SidebarMenuState {
final String menu;
const SidebarMenuSuccess(this.menu);
@override
List<Object> get props => [menu];
}

class SidebarMenuError extends SidebarMenuState {
final String errMessage;
const SidebarMenuError(this.errMessage);
@override
List<Object> get props => [errMessage];
}

Step 3: Creating a Class for Menu Navigation

Here, a class named ‘ScreensView’ is being created, which serves as an intermediary for menu switching by receiving the selected menu state parameter from the user’s click. In this example, a switch case is employed to route it to the respective menu page widget.

class ScreensView extends StatelessWidget {
final String menu;
const ScreensView({Key? key, required this.menu}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget page;
switch (menu) {
case 'Dashboard':
page = const Center(
child: Text(
"Dashboard Page",
style: TextStyle(
color: Color(0xFF171719),
fontSize: 22,
),
),
);
break;
case 'Dart':
page = const Center(
child: Text(
"Dart Page",
style: TextStyle(
color: Color(0xFF171719),
fontSize: 22,
),
),
);
break;
default:
page = const Center(
child: Text(
"Other Page",
style: TextStyle(
color: Color(0xFF171719),
fontSize: 22,
),
),
);
}
return page;
}
}

Step 4: Creating the Sidebar Menu

Then, this is where all the steps we’ve previously taken are combined and integrated to create a complete sidebar menu.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:animated_tree_view/animated_tree_view.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:sidebar_menu/bloc/sidebar_menu_bloc/sidebar_menu_bloc.dart';
import 'package:sidebar_menu/bloc/sidebar_menu_bloc/sidebar_menu_event.dart';
import 'package:sidebar_menu/bloc/sidebar_menu_bloc/sidebar_menu_state.dart';

class SidebarMenu extends StatelessWidget {
const SidebarMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
builder: (BuildContext context, Widget? child) => MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarMenuBloc()
..add(FetchSidebarMenuEvent(menu: "Dashboard")),
),
],
child: Scaffold(
backgroundColor: const Color(0xFFe2e1e4),
body: BlocBuilder<SidebarMenuBloc, SidebarMenuState>(
builder: (context, state) {
if (state is SidebarMenuSuccess) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: const Color(0xFF171719),
width: 250,
child: Column(
children: [
// Widget at the top side of the sidebar (preference).
Container(
color: const Color(0xFF171719),
width: 250,
child: Padding(
padding: const EdgeInsets.only(
top: 15,
left: 12,
right: 12,
bottom: 15,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"SidebarMenu",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
// Sidebar menu widget
Expanded(
child: Container(
color: const Color(0xFF171719),
child: TreeView.simple(
tree: menuTree,
indentation: const Indentation(width: 0),
expansionIndicatorBuilder: (context, node) {
return ChevronIndicator.rightDown(
alignment: Alignment.centerLeft,
tree: node,
color: Colors.white,
icon: Icons.arrow_right,
);
},
onItemTap: (item) {
BlocProvider.of<SidebarMenuBloc>(context).add(
FetchSidebarMenuEvent(menu: item.key));
},
builder: (context, node) {
final isSelected = state.menu == node.key;
final isExpanded = node.isExpanded;
return MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
color: node.level >= 2 || isExpanded
? const Color(
0xFF313136) // For coloring the background of child nodes
: const Color(0xFF171719),
height:
42, // Padding between one menu and another.
width: 250,
alignment: Alignment.center,
child: Padding(
padding: node.level >= 2
? const EdgeInsets.only(
left:
27) // Padding for the children of the node
: const EdgeInsets.only(left: 0),
child: Container(
width: 250,
height:
45, // The size dimension of the active button
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: isSelected
? node.isLeaf
? const Color(
0xFF2c45e8) // The color for the active node.
: null
: null,
borderRadius:
const BorderRadius.only(
topLeft: Radius.circular(
50,
),
bottomLeft: Radius.circular(
50,
),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 25,
),
child: node.level >= 2
? Text(
node.key,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
)
: Row(
children: [
Icon(
node.data,
size: 20,
color: Colors.white,
),
const SizedBox(
width: 6,
),
Text(
node.key,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
)
],
),
),
),
),
),
);
},
),
),
),
],
),
),
Expanded(
child: ScreensView(menu: state.menu),
),
],
);
} else if (state is SidebarMenuError) {
return const Center(
child: Text(
"An error has occurred. Please try again later.",
style: TextStyle(
color: Colors.white,
fontSize: 10,
),
),
);
} else {
return const SizedBox.shrink();
}
},
),
),
),
);
}
}

Closing

Using a tree-based sidebar menu can be beneficial for menus that require a clear structure, especially for menus with many complex child elements. The above implementation example still uses static data, but one of the advantages of a sidebar tree menu is its compatibility with dynamic data that can be fetched through APIs (if you are interested in a more advanced implementation, I will continue it in the next article).
For the source code documentation, you can access it here.

Reference

https://pub.dev/documentation/animated_tree_view/latest/

--

--