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

Avis

Sidebar Collapsible — React Sidebar Section — Incubator