Retour au catalogue

Sidebar Dashboard

Sidebar dashboard avec groupes collapsibles, badges et avatar.

sidebarsimple Both Responsive a11y
minimaluniversalstacked
Theme
"use client";

import { useState } from "react";
import {
  LayoutDashboard, BarChart3, Users, Settings, FileText,
  ChevronDown, Bell, LogOut, Inbox, CreditCard, Shield,
} from "lucide-react";

interface NavItem {
  label: string;
  icon: React.ElementType;
  badge?: string;
  href?: string;
  children?: { label: string; href: string }[];
}

const navItems: NavItem[] = [
  { label: "Dashboard", icon: LayoutDashboard, href: "#" },
  { label: "Analytics", icon: BarChart3, badge: "New", href: "#" },
  {
    label: "Team",
    icon: Users,
    children: [
      { label: "Members", href: "#" },
      { label: "Roles", href: "#" },
      { label: "Invitations", href: "#" },
    ],
  },
  { label: "Inbox", icon: Inbox, badge: "12", href: "#" },
  { label: "Documents", icon: FileText, href: "#" },
  {
    label: "Settings",
    icon: Settings,
    children: [
      { label: "General", href: "#" },
      { label: "Billing", href: "#" },
      { label: "Security", href: "#" },
      { label: "Notifications", href: "#" },
    ],
  },
];

const bottomItems = [
  { label: "Billing", icon: CreditCard },
  { label: "Security", icon: Shield },
];

export default function SidebarDashboard() {
  const [activeItem, setActiveItem] = useState("Dashboard");
  const [expanded, setExpanded] = useState<Set<string>>(new Set(["Team"]));

  const toggleExpand = (label: string) => {
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(label)) next.delete(label);
      else next.add(label);
      return next;
    });
  };

  return (
    <aside
      className="w-64 min-h-[600px] flex flex-col"
      style={{ background: "var(--color-background)", borderRight: "1px solid var(--color-border)" }}
    >
      {/* Brand */}
      <div className="px-5 py-5 flex items-center gap-3" style={{ borderBottom: "1px solid var(--color-border)" }}>
        <div
          className="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-bold"
          style={{ background: "var(--color-accent)", color: "var(--color-background)" }}
        >
          A
        </div>
        <div>
          <p className="text-sm font-semibold" style={{ color: "var(--color-foreground)" }}>Acme Corp</p>
          <p className="text-[10px]" style={{ color: "var(--color-foreground-light)" }}>Pro Plan</p>
        </div>
        <button className="ml-auto relative">
          <Bell size={16} style={{ color: "var(--color-foreground-muted)" }} />
          <span
            className="absolute -top-1 -right-1 w-2 h-2 rounded-full"
            style={{ background: "var(--color-accent)" }}
          />
        </button>
      </div>

      {/* Navigation */}
      <nav className="flex-1 px-3 py-4 flex flex-col gap-0.5 overflow-y-auto">
        {navItems.map((item) => {
          const Icon = item.icon;
          const isActive = activeItem === item.label;
          const isExpanded = expanded.has(item.label);
          const hasChildren = item.children && item.children.length > 0;

          return (
            <div key={item.label}>
              <button
                onClick={() => {
                  if (hasChildren) toggleExpand(item.label);
                  else setActiveItem(item.label);
                }}
                className="flex items-center gap-2.5 w-full px-3 py-2 text-sm rounded-lg transition-all duration-200"
                style={{
                  background: isActive ? "var(--color-background-alt)" : "transparent",
                  color: isActive ? "var(--color-foreground)" : "var(--color-foreground-muted)",
                }}
              >
                <Icon size={16} style={{ color: isActive ? "var(--color-accent)" : "var(--color-foreground-muted)" }} />
                <span className="flex-1 text-left font-medium">{item.label}</span>
                {item.badge && (
                  <span
                    className="px-1.5 py-0.5 text-[10px] font-bold rounded-full"
                    style={{ background: "var(--color-accent)", color: "var(--color-background)" }}
                  >
                    {item.badge}
                  </span>
                )}
                {hasChildren && (
                  <ChevronDown
                    size={14}
                    style={{
                      color: "var(--color-foreground-light)",
                      transform: isExpanded ? "rotate(180deg)" : "rotate(0)",
                      transition: "transform 0.2s",
                    }}
                  />
                )}
              </button>

              {hasChildren && isExpanded && (
                <div className="ml-5 pl-4 py-1 flex flex-col gap-0.5" style={{ borderLeft: "1px solid var(--color-border)" }}>
                  {item.children?.map((child) => (
                    <a
                      key={child.label}
                      href={child.href}
                      onClick={(e) => { e.preventDefault(); setActiveItem(child.label); }}
                      className="block px-3 py-1.5 text-[13px] rounded-md transition-colors duration-200"
                      style={{
                        color: activeItem === child.label ? "var(--color-accent)" : "var(--color-foreground-muted)",
                        background: activeItem === child.label ? "var(--color-background-alt)" : "transparent",
                      }}
                    >
                      {child.label}
                    </a>
                  ))}
                </div>
              )}
            </div>
          );
        })}

        {/* Separator */}
        <div className="my-3 h-px" style={{ background: "var(--color-border)" }} />

        {bottomItems.map((item) => {
          const Icon = item.icon;
          return (
            <button
              key={item.label}
              className="flex items-center gap-2.5 w-full px-3 py-2 text-sm rounded-lg transition-colors duration-200"
              style={{ color: "var(--color-foreground-muted)" }}
            >
              <Icon size={16} />
              <span className="font-medium">{item.label}</span>
            </button>
          );
        })}
      </nav>

      {/* User section */}
      <div className="px-4 py-4" style={{ borderTop: "1px solid var(--color-border)" }}>
        <div className="flex items-center gap-3">
          <div
            className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold"
            style={{ background: "var(--color-accent)", color: "var(--color-background)" }}
          >
            JD
          </div>
          <div className="flex-1 min-w-0">
            <p className="text-sm font-medium truncate" style={{ color: "var(--color-foreground)" }}>Jane Doe</p>
            <p className="text-[10px] truncate" style={{ color: "var(--color-foreground-light)" }}>jane@acme.com</p>
          </div>
          <button className="p-1.5 rounded-md transition-colors hover:bg-[var(--color-background-alt)]">
            <LogOut size={14} style={{ color: "var(--color-foreground-light)" }} />
          </button>
        </div>
      </div>
    </aside>
  );
}

Avis

Sidebar Dashboard — React Sidebar Section — Incubator