Retour au catalogue
Sidebar Collapsible
Sidebar avec groupes repliables, icones et animation d'expansion. Ideal pour documentation.
sidebarmedium Both Responsive a11y
minimalcorporatesaaseducationuniversalstacked
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown, Folder, FileText, Settings, Layers } from "lucide-react";
interface SidebarGroup {
label: string;
icon: string;
items: { label: string; href?: string }[];
}
interface SidebarCollapsibleProps {
title?: string;
groups?: SidebarGroup[];
}
const iconMap: Record<string, React.ElementType> = {
Folder, FileText, Settings, Layers,
};
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function SidebarCollapsible({
title = "Documentation",
groups = [],
}: SidebarCollapsibleProps) {
const [openGroups, setOpenGroups] = useState<Set<number>>(new Set([0]));
const toggleGroup = (index: number) => {
const next = new Set(openGroups);
if (next.has(index)) next.delete(index);
else next.add(index);
setOpenGroups(next);
};
return (
<aside
className="w-64 min-h-[500px] py-6 px-4"
style={{
background: "var(--color-background)",
borderRight: "1px solid var(--color-border)",
}}
>
<h3
className="px-3 mb-5 text-sm font-semibold"
style={{ color: "var(--color-foreground)" }}
>
{title}
</h3>
<div className="flex flex-col gap-0.5">
{groups.map((group, gi) => {
const Icon = iconMap[group.icon] || Folder;
const isOpen = openGroups.has(gi);
return (
<div key={gi}>
<button
onClick={() => toggleGroup(gi)}
className="flex items-center gap-2.5 w-full px-3 py-2 text-sm font-medium rounded-lg transition-colors"
style={{
color: "var(--color-foreground)",
background: isOpen ? "var(--color-background-alt)" : "transparent",
}}
>
<Icon size={16} style={{ color: "var(--color-foreground-muted)" }} />
<span className="flex-1 text-left">{group.label}</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronDown
size={14}
style={{ color: "var(--color-foreground-light)" }}
/>
</motion.div>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease }}
className="overflow-hidden"
>
<div className="pl-10 py-1 flex flex-col gap-0.5">
{group.items.map((item, ii) => (
<a
key={ii}
href={item.href || "#"}
className="block px-3 py-1.5 text-sm rounded-md transition-colors hover:bg-[var(--color-background-alt)]"
style={{ color: "var(--color-foreground-muted)" }}
>
{item.label}
</a>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</div>
</aside>
);
}