DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Building a Collapsible Admin Sidebar with React Router + useLocation (Pro Patterns)

Modern dashboards demand **navigation that’s dynamic, reactive, and context-aware

Modern dashboards demand navigation that’s dynamic, reactive, and context-aware.

In React Router apps, useLocation is your go-to hook for reading the current URL and making UI decisions — like highlighting active menu items in a sidebar.

In this article, we’ll build a collapsible Admin Sidebar with:

useLocation to detect and highlight active routes

✅ Lucide React icons for a modern look

✅ Collapsible state toggling (ChevronLeft / ChevronRight)

✅ Responsive + accessible design patterns


Why useLocation?

React Router’s useLocation hook provides an object with the current URL, including pathname, search, and hash.

In dashboards, this is typically used for:

  • Active states: Highlighting the current menu item.
  • Contextual conditions: Checking if the route matches a sub-path (/admin/products/123).
  • Debugging / Analytics: Logging path changes or sending analytics events.

Sidebar Implementation

import { Link, useLocation } from 'react-router-dom';
import {
  Home,
  Users,
  BarChart3,
  Settings,
  FileText,
  ShoppingCart,
  Bell,
  HelpCircle,
  ChevronLeft,
  ChevronRight,
} from 'lucide-react';
import { CustomLogo } from '@/components/custom/CustomLogo';

interface SidebarProps {
  isCollapsed: boolean;
  onToggle: () => void;
}

export const AdminSidebar: React.FC<SidebarProps> = ({
  isCollapsed,
  onToggle,
}) => {
  const { pathname } = useLocation();

  const menuItems = [
    { icon: Home, label: 'Dashboard', to: '/admin' },
    { icon: BarChart3, label: 'Products', to: '/admin/products' },
    { icon: Users, label: 'Users' },
    { icon: ShoppingCart, label: 'Orders' },
    { icon: FileText, label: 'Reports' },
    { icon: Bell, label: 'Notifications' },
    { icon: Settings, label: 'Settings' },
    { icon: HelpCircle, label: 'Aid' },
  ];

  const isActiveRoute = (to: string) => {
    if (pathname.includes('/admin/products/') && to === '/admin/products') {
      return true;
    }
    return pathname === to;
  };

  return (
    <div
      className={`bg-white border-r border-gray-200 transition-all duration-300 ease-in-out ${
        isCollapsed ? 'w-18' : 'w-64'
      } flex flex-col`}
    >
      {/* Header */}
      <div className="p-4 border-b border-gray-200 flex items-center justify-between h-18">
        {!isCollapsed && <CustomLogo />}
        <button
          onClick={onToggle}
          className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
        >
          {isCollapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
        </button>
      </div>

      {/* Navigation */}
      <nav className="flex-1 p-4">
        <ul className="space-y-2">
          {menuItems.map((item, index) => {
            const Icon = item.icon;
            return (
              <li key={index}>
                <Link
                  to={item.to || '/admin'}
                  className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-all duration-200 group ${
                    isActiveRoute(item.to || '/xxxx')
                      ? 'bg-blue-50 text-blue-600 border-r-2 border-blue-600'
                      : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
                  }`}
                >
                  <Icon size={20} className="flex-shrink-0" />
                  {!isCollapsed && (
                    <span className="font-medium">{item.label}</span>
                  )}
                </Link>
              </li>
            );
          })}
        </ul>
      </nav>

      {/* User Profile */}
      {!isCollapsed && (
        <div className="p-4 border-t border-gray-200">
          <div className="flex items-center space-x-3 p-3 rounded-lg hover:bg-gray-50 transition-colors cursor-pointer">
            <div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
              JD
            </div>
            <div className="flex-1 min-w-0">
              <p className="text-sm font-medium text-gray-900 truncate">
                John Doe
              </p>
              <p className="text-xs text-gray-500 truncate">john@company.com</p>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Key Concepts in This Example

Feature Purpose
useLocation Reads current pathname to decide active state
isActiveRoute() Custom logic for exact vs. nested matches
Lucide Icons Lightweight, consistent icon system (lucide-react)
Collapsible sidebar UX pattern for saving screen real estate
TailwindCSS classes Handles spacing, borders, hover states, and transitions

Advanced Patterns

  • Persistent collapse state: Store isCollapsed in localStorage or context to remember user preferences.
  • Role-based menus: Filter menuItems based on user roles.
  • Breadcrumbs: Derive breadcrumbs from pathname.split('/').
  • Analytics: Fire events whenever pathname changes with a useEffect on location.

Summary

With just a few hooks and thoughtful design, you can build:

  • A responsive, collapsible admin sidebar
  • Context-aware active state highlighting
  • A scalable pattern for menu item configuration

This approach makes your dashboard navigation clean, declarative, and future-proof. ⚡

Top comments (1)

Collapse
 
ariansj profile image
Arian Seyedi

Absolutely love how you broke this down!

I’ve built a few admin dashboards myself, and using useLocation for active route highlighting is such a clean approach، way better than juggling state manually. Also, making the sidebar collapsible while keeping it responsive and context-aware really improves UX, especially on complex dashboards.

One extra tip from my experience: persisting the collapse state in localStorage or context makes it feel even more polished for returning users. Curious، have you experimented with role-based menu filtering for multi-role dashboards?