Retour au catalogue

Sidebar Collapsible Icons

Sidebar repliable qui bascule entre icones seules et mode etendu avec labels et tooltips.

sidebarmedium Both Responsive a11y
minimalcorporatesaassaassplit
Theme
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
  Home,
  BarChart3,
  Users,
  Settings,
  HelpCircle,
  ChevronLeft,
  ChevronRight,
  Bell,
  FolderOpen,
  Zap,
  LogOut,
} from "lucide-react";

interface NavItem {
  label: string;
  icon: string;
  badge?: number;
  active?: boolean;
}

interface NavSection {
  title?: string;
  items: NavItem[];
}

interface SidebarCollapsibleIconsProps {
  sections?: NavSection[];
  userName?: string;
  userRole?: string;
}

const iconMap: Record<string, React.ElementType> = {
  Home,
  BarChart3,
  Users,
  Settings,
  HelpCircle,
  Bell,
  FolderOpen,
  Zap,
  LogOut,
};

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

export default function SidebarCollapsibleIcons({
  sections = [],
  userName = "Utilisateur",
  userRole = "Admin",
}: SidebarCollapsibleIconsProps) {
  const [isExpanded, setIsExpanded] = useState(true);
  const [hoveredItem, setHoveredItem] = useState<string | null>(null);

  return (
    <motion.aside
      animate={{ width: isExpanded ? 260 : 72 }}
      transition={{ duration: 0.3, ease }}
      className="relative min-h-[600px] flex flex-col py-4 overflow-hidden"
      style={{
        background: "var(--color-background-card)",
        borderRight: "1px solid var(--color-border)",
      }}
    >
      {/* Header / Brand */}
      <div className="flex items-center gap-3 px-4 mb-6">
        <div
          className="w-9 h-9 rounded-xl flex items-center justify-center shrink-0"
          style={{ background: "var(--color-accent)" }}
        >
          <Zap size={18} style={{ color: "var(--color-background)" }} />
        </div>
        <AnimatePresence>
          {isExpanded && (
            <motion.span
              initial={{ opacity: 0, width: 0 }}
              animate={{ opacity: 1, width: "auto" }}
              exit={{ opacity: 0, width: 0 }}
              className="text-sm font-bold whitespace-nowrap overflow-hidden"
              style={{ color: "var(--color-foreground)" }}
            >
              MonApp
            </motion.span>
          )}
        </AnimatePresence>
      </div>

      {/* Nav sections */}
      <div className="flex-1 flex flex-col gap-4 px-2">
        {sections.map((section, si) => (
          <div key={si}>
            <AnimatePresence>
              {isExpanded && section.title && (
                <motion.div
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  className="px-3 mb-1 text-[10px] font-semibold uppercase tracking-widest"
                  style={{ color: "var(--color-foreground-light)" }}
                >
                  {section.title}
                </motion.div>
              )}
            </AnimatePresence>

            <div className="flex flex-col gap-0.5">
              {section.items.map((item) => {
                const Icon = iconMap[item.icon] || Home;
                const isHovered = hoveredItem === item.label;

                return (
                  <div
                    key={item.label}
                    className="relative"
                    onMouseEnter={() => setHoveredItem(item.label)}
                    onMouseLeave={() => setHoveredItem(null)}
                  >
                    <motion.button
                      className="flex items-center gap-3 w-full rounded-xl transition-all"
                      style={{
                        padding: isExpanded ? "10px 12px" : "10px",
                        justifyContent: isExpanded ? "flex-start" : "center",
                        background: item.active
                          ? "var(--color-accent-subtle)"
                          : "transparent",
                        color: item.active
                          ? "var(--color-accent)"
                          : "var(--color-foreground-muted)",
                      }}
                      whileHover={{
                        background: "var(--color-background-alt)",
                      }}
                    >
                      <div className="relative shrink-0">
                        <Icon size={20} />
                        {item.badge && !isExpanded && (
                          <div
                            className="absolute -top-1 -right-1 w-4 h-4 rounded-full text-[9px] font-bold flex items-center justify-center"
                            style={{
                              background: "var(--color-accent)",
                              color: "var(--color-background)",
                            }}
                          >
                            {item.badge}
                          </div>
                        )}
                      </div>

                      <AnimatePresence>
                        {isExpanded && (
                          <motion.div
                            initial={{ opacity: 0, width: 0 }}
                            animate={{ opacity: 1, width: "auto" }}
                            exit={{ opacity: 0, width: 0 }}
                            className="flex items-center justify-between flex-1 overflow-hidden whitespace-nowrap"
                          >
                            <span className="text-sm">{item.label}</span>
                            {item.badge && (
                              <span
                                className="text-[10px] px-1.5 py-0.5 rounded-full font-medium"
                                style={{
                                  background: "var(--color-accent)",
                                  color: "var(--color-background)",
                                }}
                              >
                                {item.badge}
                              </span>
                            )}
                          </motion.div>
                        )}
                      </AnimatePresence>
                    </motion.button>

                    {/* Tooltip when collapsed */}
                    <AnimatePresence>
                      {!isExpanded && isHovered && (
                        <motion.div
                          initial={{ opacity: 0, x: -4 }}
                          animate={{ opacity: 1, x: 0 }}
                          exit={{ opacity: 0, x: -4 }}
                          className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-1.5 rounded-lg text-xs font-medium whitespace-nowrap z-50"
                          style={{
                            background: "var(--color-foreground)",
                            color: "var(--color-background)",
                          }}
                        >
                          {item.label}
                        </motion.div>
                      )}
                    </AnimatePresence>
                  </div>
                );
              })}
            </div>
          </div>
        ))}
      </div>

      {/* User section */}
      <div
        className="mx-2 mt-4 px-3 py-3 rounded-xl flex items-center gap-3"
        style={{ background: "var(--color-background-alt)" }}
      >
        <div
          className="w-8 h-8 rounded-full flex items-center justify-center shrink-0 text-xs font-bold"
          style={{
            background: "var(--color-accent)",
            color: "var(--color-background)",
          }}
        >
          {userName.charAt(0)}
        </div>
        <AnimatePresence>
          {isExpanded && (
            <motion.div
              initial={{ opacity: 0, width: 0 }}
              animate={{ opacity: 1, width: "auto" }}
              exit={{ opacity: 0, width: 0 }}
              className="overflow-hidden whitespace-nowrap"
            >
              <div
                className="text-sm font-medium"
                style={{ color: "var(--color-foreground)" }}
              >
                {userName}
              </div>
              <div
                className="text-[11px]"
                style={{ color: "var(--color-foreground-muted)" }}
              >
                {userRole}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {/* Toggle button */}
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        className="absolute top-6 -right-3 w-6 h-6 rounded-full flex items-center justify-center z-10"
        style={{
          background: "var(--color-background-card)",
          border: "1px solid var(--color-border)",
          color: "var(--color-foreground-muted)",
        }}
      >
        {isExpanded ? <ChevronLeft size={14} /> : <ChevronRight size={14} />}
      </button>
    </motion.aside>
  );
}

Avis

Sidebar Collapsible Icons — React Sidebar Section — Incubator