easy_sidemenu 1.0.0 copy "easy_sidemenu: ^1.0.0" to clipboard
easy_sidemenu: ^1.0.0 copied to clipboard

An easy to use side menu (navigation rail) in flutter and can used for navigation

example/lib/main.dart

import 'package:easy_sidemenu/easy_sidemenu.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

// ── Theme presets ─────────────────────────────────────────────────────────────

class _Preset {
  const _Preset(this.label, this.theme, {this.background, this.outerPadding});
  final String label;
  final SideMenuThemeData theme;

  /// Optional background decoration displayed behind the whole scaffold Row
  /// (used by glass/frozen themes to show through the blurred menu).
  final BoxDecoration? background;

  /// When set, the SideMenu is wrapped in [Padding] with this value — used
  /// for the floating Card style where the menu hovers above the background.
  final EdgeInsets? outerPadding;
}

final _presets = [
  const _Preset('Ocean', _ocean),
  const _Preset('Midnight', _midnight),
  const _Preset('Aurora', _aurora),
  const _Preset('Minimal', _minimal),
  _glassPreset,
  _frozenPreset,
  _cardPreset,
];

// Non-const presets that use runtime Color.withValues / LinearGradient
final _glassPreset = _Preset(
  'Glass',
  SideMenuThemeData(
    displayMode: SideMenuDisplayMode.auto,
    openWidth: 240,
    compactWidth: 68,
    backdropSigma: 18,
    menuDecoration: BoxDecoration(
      color: Colors.white.withValues(alpha: 0.18),
      border: Border(
        right:
            BorderSide(color: Colors.white.withValues(alpha: 0.30), width: 1),
      ),
    ),
    selectedColor: Colors.white.withValues(alpha: 0.30),
    selectedIconColor: Colors.white,
    selectedTitleStyle: const TextStyle(
      color: Colors.white,
      fontWeight: FontWeight.w600,
      fontSize: 15,
    ),
    unselectedIconColor: Colors.white70,
    unselectedTitleStyle: const TextStyle(color: Colors.white70, fontSize: 15),
    hoverColor: Colors.white.withValues(alpha: 0.12),
    itemBorderRadius: const BorderRadius.all(Radius.circular(10)),
    itemOuterPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
    toggleColor: Colors.white70,
    expansionArrowColor: Colors.white70,
    expansionArrowOpenColor: Colors.white,
  ),
  background: const BoxDecoration(
    gradient: LinearGradient(
      colors: [
        Color.fromARGB(255, 24, 24, 39),
        Color(0xFF16213E),
        Color.fromARGB(255, 19, 69, 129)
      ],
      begin: Alignment.topLeft,
      end: Alignment.bottomLeft,
    ),
  ),
);

final _frozenPreset = _Preset(
  'Frozen',
  SideMenuThemeData(
    displayMode: SideMenuDisplayMode.auto,
    openWidth: 240,
    compactWidth: 68,
    backdropSigma: 24,
    menuDecoration: BoxDecoration(
      color: const Color(0xFFE8F4FD).withValues(alpha: 0.22),
      border: Border(
        right: BorderSide(
            color: const Color(0xFFB3D9F2).withValues(alpha: 0.50), width: 1),
      ),
    ),
    selectedColor: const Color(0xFF2196F3).withValues(alpha: 0.25),
    selectedItemDecoration: BoxDecoration(
      color: const Color(0xFF2196F3).withValues(alpha: 0.20),
      borderRadius: const BorderRadius.all(Radius.circular(10)),
      border: Border.all(
          color: const Color(0xFF2196F3).withValues(alpha: 0.50), width: 1),
    ),
    selectedIconColor: const Color(0xFF0D47A1),
    selectedTitleStyle: const TextStyle(
      color: Color(0xFF0D47A1),
      fontWeight: FontWeight.w600,
      fontSize: 15,
    ),
    unselectedIconColor: const Color(0xFF546E7A),
    unselectedTitleStyle:
        const TextStyle(color: Color(0xFF546E7A), fontSize: 15),
    hoverColor: const Color(0xFF90CAF9).withValues(alpha: 0.20),
    itemBorderRadius: const BorderRadius.all(Radius.circular(10)),
    itemOuterPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
    toggleColor: const Color(0xFF546E7A),
    expansionArrowColor: const Color(0xFF546E7A),
    expansionArrowOpenColor: const Color(0xFF1565C0),
  ),
  background: const BoxDecoration(
    gradient: LinearGradient(
      colors: [Color(0xFFE3F2FD), Color(0xFFBBDEFB), Color(0xFF90CAF9)],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  ),
);

// 5 ── Card: floating rounded panel, shadow, border — modern sidebar
final _cardPreset = _Preset(
  'Card',
  SideMenuThemeData(
    displayMode: SideMenuDisplayMode.auto,
    openWidth: 240,
    compactWidth: 68,
    menuDecoration: BoxDecoration(
      color: Colors.white,
      borderRadius: const BorderRadius.all(Radius.circular(16)),
      border: Border.all(color: const Color(0xFFE2E8F0)),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.08),
          blurRadius: 24,
          spreadRadius: 0,
          offset: const Offset(4, 0),
        ),
      ],
    ),
    selectedItemDecoration: const BoxDecoration(
      color: Color(0xFFEFF6FF),
      borderRadius: BorderRadius.all(Radius.circular(10)),
      border: Border.fromBorderSide(BorderSide(color: Color(0xFFBFDBFE))),
    ),
    selectedIconColor: const Color(0xFF2563EB),
    selectedTitleStyle: const TextStyle(
      color: Color(0xFF1D4ED8),
      fontWeight: FontWeight.w600,
      fontSize: 15,
    ),
    unselectedIconColor: const Color(0xFF64748B),
    unselectedTitleStyle:
        const TextStyle(color: Color(0xFF64748B), fontSize: 15),
    hoverColor: const Color(0xFFF8FAFC),
    itemBorderRadius: const BorderRadius.all(Radius.circular(10)),
    itemOuterPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
    toggleColor: const Color(0xFF94A3B8),
    expansionArrowColor: const Color(0xFF94A3B8),
    expansionArrowOpenColor: const Color(0xFF2563EB),
  ),
  outerPadding: const EdgeInsets.all(8),
);

// 1 ── Ocean: light background, blue pill, rounded items
const _ocean = SideMenuThemeData(
  displayMode: SideMenuDisplayMode.auto,
  openWidth: 240,
  compactWidth: 68,
  selectedColor: Color(0xFF2563EB),
  selectedIconColor: Colors.white,
  selectedTitleStyle: TextStyle(
    color: Colors.white,
    fontWeight: FontWeight.w600,
    fontSize: 15,
  ),
  unselectedIconColor: Color(0xFF64748B),
  unselectedTitleStyle: TextStyle(color: Color(0xFF64748B), fontSize: 15),
  itemBorderRadius: BorderRadius.all(Radius.circular(12)),
  itemOuterPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
  hoverColor: Color(0xFFEFF6FF),
);

// 2 ── Midnight: dark sidebar with indigo accent
const _midnight = SideMenuThemeData(
  displayMode: SideMenuDisplayMode.auto,
  openWidth: 240,
  compactWidth: 68,
  menuDecoration: BoxDecoration(color: Color(0xFF0F172A)),
  selectedItemDecoration: BoxDecoration(
    color: Color(0xFF1E293B),
    borderRadius: BorderRadius.all(Radius.circular(10)),
  ),
  selectedIconColor: Color(0xFF818CF8),
  selectedTitleStyle: TextStyle(
    color: Colors.white,
    fontWeight: FontWeight.w600,
    fontSize: 15,
  ),
  unselectedIconColor: Color(0xFF475569),
  unselectedTitleStyle: TextStyle(color: Color(0xFF475569), fontSize: 15),
  hoverColor: Color(0xFF1E293B),
  itemOuterPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
  toggleColor: Color(0xFF94A3B8),
  expansionArrowColor: Color(0xFF475569),
  expansionArrowOpenColor: Color(0xFF818CF8),
);

// 3 ── Aurora: purple-to-indigo gradient sidebar
const _aurora = SideMenuThemeData(
  displayMode: SideMenuDisplayMode.auto,
  openWidth: 240,
  compactWidth: 68,
  menuDecoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Color(0xFF667EEA), Color(0xFF764BA2)],
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    ),
  ),
  selectedColor: Color(0x33FFFFFF),
  selectedIconColor: Colors.white,
  selectedTitleStyle: TextStyle(
    color: Colors.white,
    fontWeight: FontWeight.w600,
    fontSize: 15,
  ),
  unselectedIconColor: Color(0xCCFFFFFF),
  unselectedTitleStyle: TextStyle(color: Color(0xCCFFFFFF), fontSize: 15),
  hoverColor: Color(0x1AFFFFFF),
  itemBorderRadius: BorderRadius.all(Radius.circular(10)),
  itemOuterPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
  toggleColor: Colors.white70,
  expansionArrowColor: Color(0xCCFFFFFF),
  expansionArrowOpenColor: Colors.white,
);

// 4 ── Minimal: white background, left-border active indicator, no radius
const _minimal = SideMenuThemeData(
  displayMode: SideMenuDisplayMode.auto,
  openWidth: 240,
  compactWidth: 68,
  backgroundColor: Color(0xFFF8FAFC),
  selectedItemDecoration: BoxDecoration(
    color: Color(0xFFEFF6FF),
    border: Border(
      left: BorderSide(color: Color(0xFF2563EB), width: 3),
    ),
  ),
  selectedIconColor: Color(0xFF2563EB),
  selectedTitleStyle: TextStyle(
    color: Color(0xFF1E40AF),
    fontWeight: FontWeight.w600,
    fontSize: 15,
  ),
  unselectedIconColor: Color(0xFF94A3B8),
  unselectedTitleStyle: TextStyle(color: Color(0xFF64748B), fontSize: 15),
  hoverColor: Color(0xFFF1F5F9),
  itemBorderRadius: BorderRadius.zero,
  itemOuterPadding: EdgeInsets.zero,
);

// ── App ───────────────────────────────────────────────────────────────────────

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'easy_sidemenu Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final _controller = SideMenuController();
  final _pageController = PageController();
  int _presetIndex = 0;

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      _pageController.jumpToPage(_controller.currentIndex);
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final preset = _presets[_presetIndex];
    final isDark = preset.label == 'Midnight' ||
        preset.label == 'Aurora' ||
        preset.label == 'Glass';
    final isCard = preset.label == 'Card';

    Widget sideMenuWidget = SideMenu(
      controller: _controller,
      theme: preset.theme,
      alwaysShowFooter: isCard,
      title: _SideMenuHeader(
        presetLabel: preset.label,
        isDark: isDark,
        onNext: () =>
            setState(() => _presetIndex = (_presetIndex + 1) % _presets.length),
      ),
      footer: isCard ? const _CardFooter() : _SideMenuFooter(isDark: isDark),
      items: [
        SideMenuItem(
          title: 'Dashboard',
          icon: const Icon(Icons.dashboard_rounded),
          badge: const Text('3',
              style: TextStyle(color: Colors.white, fontSize: 10)),
          tooltipContent: 'Dashboard',
          onTap: (i, c) => c.goTo(i),
        ),
        SideMenuItem(
          title: 'Users',
          icon: const Icon(Icons.people_rounded),
          onTap: (i, c) => c.goTo(i),
        ),
        SideMenuExpansionItem(
          title: 'Projects',
          icon: const Icon(Icons.folder_rounded),
          children: [
            SideMenuItem(
              title: 'Active',
              icon: const Icon(Icons.circle, size: 10),
              onTap: (i, c) => c.goTo(i),
            ),
            SideMenuItem(
              title: 'Archived',
              icon: const Icon(Icons.circle_outlined, size: 10),
              onTap: (i, c) => c.goTo(i),
            ),
          ],
        ),
        SideMenuItem(
          title: 'Files',
          icon: const Icon(Icons.folder_copy_rounded),
          trailing: Container(
            decoration: const BoxDecoration(
              color: Colors.amber,
              borderRadius: BorderRadius.all(Radius.circular(6)),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
            child: const Text('New',
                style: TextStyle(fontSize: 10, color: Colors.white)),
          ),
          onTap: (i, c) => c.goTo(i),
        ),
        SideMenuItem(
          title: 'Analytics',
          icon: const Icon(Icons.bar_chart_rounded),
          onTap: (i, c) => c.goTo(i),
        ),
        const SideMenuItem(builder: _dividerBuilder),
        SideMenuItem(
          title: 'Settings',
          icon: const Icon(Icons.settings_rounded),
          onTap: (i, c) => c.goTo(i),
        ),
        const SideMenuItem(
          title: 'Sign out',
          icon: Icon(Icons.logout_rounded),
        ),
      ],
    );

    if (preset.outerPadding != null) {
      sideMenuWidget =
          Padding(padding: preset.outerPadding!, child: sideMenuWidget);
    }

    final body = Row(
      children: [
        sideMenuWidget,
        if (!isCard) const VerticalDivider(width: 0),
        Expanded(
          child: PageView(
            controller: _pageController,
            physics: const NeverScrollableScrollPhysics(),
            children: const [
              _Page('Dashboard', Icons.dashboard_rounded),
              _Page('Users', Icons.people_rounded),
              SizedBox.shrink(),
              _Page('Active projects', Icons.circle),
              _Page('Archived projects', Icons.circle_outlined),
              _Page('Files', Icons.folder_copy_rounded),
              _Page('Analytics', Icons.bar_chart_rounded),
              SizedBox.shrink(),
              _Page('Settings', Icons.settings_rounded),
              _Page('Sign out', Icons.logout_rounded),
            ],
          ),
        ),
      ],
    );

    final bg = preset.background;
    return Scaffold(
      backgroundColor: isCard ? const Color(0xFFF1F5F9) : null,
      body: bg != null ? Container(decoration: bg, child: body) : body,
    );
  }
}

Widget _dividerBuilder(BuildContext context, SideMenuDisplayMode mode) =>
    const Divider(indent: 8, endIndent: 8, height: 24);

// ── Side menu header ──────────────────────────────────────────────────────────

class _SideMenuHeader extends StatelessWidget {
  const _SideMenuHeader({
    required this.presetLabel,
    required this.isDark,
    required this.onNext,
  });

  final String presetLabel;
  final bool isDark;
  final VoidCallback onNext;

  @override
  Widget build(BuildContext context) {
    final textColor = isDark ? Colors.white70 : Colors.black87;
    return Padding(
      padding: const EdgeInsets.fromLTRB(12, 16, 8, 8),
      child: Row(
        children: [
          Container(
            width: 32,
            height: 32,
            decoration: BoxDecoration(
              color: const Color(0xFF2563EB),
              borderRadius: BorderRadius.circular(8),
            ),
            child: const Icon(Icons.bolt, color: Colors.white, size: 20),
          ),
          const SizedBox(width: 10),
          Expanded(
            child: Text(
              'easy_sidemenu',
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 14,
                color: textColor,
              ),
              overflow: TextOverflow.ellipsis,
            ),
          ),
          IconButton(
            icon: Icon(Icons.palette_outlined, size: 18, color: textColor),
            tooltip: 'Switch theme ($presetLabel)',
            onPressed: onNext,
            padding: EdgeInsets.zero,
            constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
          ),
        ],
      ),
    );
  }
}

// ── Side menu footer ──────────────────────────────────────────────────────────

class _SideMenuFooter extends StatelessWidget {
  const _SideMenuFooter({required this.isDark});
  final bool isDark;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10),
      child: Row(
        children: [
          const CircleAvatar(
            radius: 16,
            backgroundColor: Color(0xFF2563EB),
            child: Text('M',
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 13,
                    fontWeight: FontWeight.bold)),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('Mohammad 🇮🇷',
                    style: TextStyle(
                        fontSize: 13,
                        fontWeight: FontWeight.w600,
                        color: isDark ? Colors.white : Colors.black87),
                    overflow: TextOverflow.ellipsis),
                Text('Admin',
                    style: TextStyle(
                        fontSize: 11,
                        color: isDark
                            ? const Color(0xFF94A3B8)
                            : Colors.grey.shade600)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ── Card footer (floating preset) ────────────────────────────────────────────

class _CardFooter extends StatelessWidget {
  const _CardFooter();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Divider(height: 16, color: Color(0xFFE2E8F0)),
          const _CardAction(
              icon: Icons.add_circle_outline_rounded,
              label: 'New Project',
              color: Color(0xFF2563EB)),
          // const _CardAction(
          //     icon: Icons.search_rounded,
          //     label: 'Search',
          //     color: Color(0xFF64748B)),
          // const _CardAction(
          //     icon: Icons.people_outline_rounded,
          //     label: 'Invite teammate',
          //     color: Color(0xFF64748B)),
          // const _CardAction(
          //     icon: Icons.keyboard_rounded,
          //     label: 'Shortcuts',
          //     color: Color(0xFF94A3B8)
          // ),
          const SizedBox(height: 6),
          const SizedBox(height: 2),
          Container(
            decoration: BoxDecoration(
              color: const Color(0xFFF1F5F9),
              borderRadius: BorderRadius.circular(10),
              border: Border.all(color: const Color(0xFFE2E8F0)),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
            child: const Row(
              children: [
                CircleAvatar(
                  radius: 14,
                  backgroundColor: Color(0xFF2563EB),
                  child: Text('M',
                      style: TextStyle(
                          color: Colors.white,
                          fontSize: 12,
                          fontWeight: FontWeight.bold)),
                ),
                SizedBox(width: 8),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text('Mohammad',
                          style: TextStyle(
                              fontSize: 12,
                              fontWeight: FontWeight.w600,
                              color: Color(0xFF1E293B)),
                          overflow: TextOverflow.ellipsis),
                      Text('Pro Plan',
                          style: TextStyle(
                              fontSize: 10, color: Color(0xFF64748B))),
                    ],
                  ),
                ),
                Icon(Icons.unfold_more_rounded,
                    size: 16, color: Color(0xFF94A3B8)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _CardAction extends StatelessWidget {
  const _CardAction(
      {required this.icon, required this.label, required this.color});
  final IconData icon;
  final String label;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {},
      borderRadius: BorderRadius.circular(8),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 7),
        child: Row(
          children: [
            Icon(icon, size: 17, color: color),
            const SizedBox(width: 10),
            Text(label,
                style: const TextStyle(fontSize: 13, color: Color(0xFF475569))),
          ],
        ),
      ),
    );
  }
}

// ── Page ──────────────────────────────────────────────────────────────────────

class _Page extends StatelessWidget {
  const _Page(this.label, this.icon);
  final String label;
  final IconData icon;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 48, color: Colors.grey.shade300),
          const SizedBox(height: 16),
          Text(label,
              style: TextStyle(
                  fontSize: 28,
                  fontWeight: FontWeight.w300,
                  color: Colors.grey.shade600)),
        ],
      ),
    );
  }
}
458
likes
160
points
6.41k
downloads

Documentation

API reference

Publisher

verified publisherjamalianpour.ir

Weekly Downloads

An easy to use side menu (navigation rail) in flutter and can used for navigation

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on easy_sidemenu