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>
);
}