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

Avis

Breadcrumb Dropdown — React Breadcrumb Section — Incubator