Retour au catalogue
Breadcrumb Dropdown
Fil d'Ariane avec menus deroulants pour les niveaux intermediaires et collapse automatique.
breadcrumbmedium Both Responsive a11y
minimalcorporateecommercesaasuniversalstacked
Theme
"use client";
import { useState, useRef, useEffect } from "react";
import { ChevronRight, MoreHorizontal } from "lucide-react";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbDropdownProps {
items?: BreadcrumbItem[];
}
const mockItems: BreadcrumbItem[] = [
{ label: "Accueil", href: "#" },
{ label: "Catalogue", href: "#" },
{ label: "Vetements", href: "#" },
{ label: "Homme", href: "#" },
{ label: "Vestes", href: "#" },
{ label: "Parka hiver" },
];
export default function BreadcrumbDropdown({
items = mockItems,
}: BreadcrumbDropdownProps) {
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLLIElement>(null);
const collapsed = items.length > 4;
const first = collapsed ? items[0] : null;
const hidden = collapsed ? items.slice(1, -2) : [];
const tail = collapsed ? items.slice(-2) : [];
useEffect(() => {
const handler = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
const renderItem = (item: BreadcrumbItem, isLast: boolean) =>
isLast ? (
<span
className="font-medium"
style={{ color: "var(--color-foreground)" }}
aria-current="page"
>
{item.label}
</span>
) : (
<a
href={item.href || "#"}
className="transition-colors duration-200 hover:underline underline-offset-2"
style={{ color: "var(--color-foreground-muted)" }}
>
{item.label}
</a>
);
const separator = (
<ChevronRight
size={14}
className="shrink-0"
style={{ color: "var(--color-foreground-light)" }}
/>
);
return (
<nav
className="py-4 px-6"
style={{ background: "var(--color-background)" }}
aria-label="Fil d'Ariane"
>
<ol className="flex items-center gap-1.5 text-sm">
{collapsed ? (
<>
<li className="flex items-center gap-1.5">
{renderItem(first!, false)}
{separator}
</li>
<li className="relative flex items-center gap-1.5" ref={dropdownRef}>
<button
onClick={() => setOpen(!open)}
className="px-1.5 py-0.5 rounded transition-colors duration-200 cursor-pointer"
style={{
color: "var(--color-foreground-muted)",
background: open ? "var(--color-background-alt)" : "transparent",
}}
aria-label="Afficher les elements masques"
>
<MoreHorizontal size={16} />
</button>
<div
className="absolute top-full left-0 mt-1 min-w-[180px] py-1 rounded-lg shadow-xl z-10 transition-all duration-200 origin-top-left"
style={{
background: "var(--color-background-card)",
border: "1px solid var(--color-border)",
opacity: open ? 1 : 0,
transform: open ? "scale(1) translateY(0)" : "scale(0.95) translateY(-4px)",
pointerEvents: open ? "auto" : "none",
}}
>
{hidden.map((item, i) => (
<a
key={i}
href={item.href || "#"}
className="block px-3 py-2 text-sm transition-colors duration-150"
style={{ color: "var(--color-foreground-muted)" }}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = "var(--color-background-alt)";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = "transparent";
}}
>
{item.label}
</a>
))}
</div>
{separator}
</li>
{tail.map((item, i) => (
<li key={i} className="flex items-center gap-1.5">
{renderItem(item, i === tail.length - 1)}
{i < tail.length - 1 && separator}
</li>
))}
</>
) : (
items.map((item, i) => (
<li key={i} className="flex items-center gap-1.5">
{i > 0 && separator}
{renderItem(item, i === items.length - 1)}
</li>
))
)}
</ol>
</nav>
);
}