React + MotionNavigation

Fil d’Ariane rétractable

Les étapes se réduisent à des icônes et déplient leur nom avec un ressort au survol ou au focus.

Réglages

0

Code

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Box, Folder, House, Layers, Palette } from "lucide-react";
import type { LucideIcon } from "lucide-react";

const CRUMB_PATH: { label: string; icon: LucideIcon }[] = [
  { label: "Accueil", icon: House },
  { label: "Projets", icon: Folder },
  { label: "Aimran", icon: Box },
  { label: "Design", icon: Palette },
  { label: "Navigation", icon: Layers },
];

const CRUMB_SPRING = { type: "spring", stiffness: 460, damping: 36 } as const;

interface CollapsingBreadcrumbProps {
  visible?: number;
  accent?: string;
}

export default function CollapsingBreadcrumb({
  visible = 0,
  accent = "#0070f3",
}: CollapsingBreadcrumbProps) {
  const [depth, setDepth] = useState(CRUMB_PATH.length - 1);
  const [peek, setPeek] = useState<number | null>(null);

  return (
    <nav aria-label="Fil d’Ariane" className="max-w-full">
      <motion.ol
        layout
        transition={CRUMB_SPRING}
        onPointerLeave={() => setPeek(null)}
        className="flex h-12 max-w-full items-center rounded-full bg-white px-1.5 text-[13px] text-[#1d1d1f] shadow-[0_1px_2px_rgba(0,0,0,0.04),0_10px_32px_rgba(0,0,0,0.07)] ring-1 ring-black/[0.06] dark:bg-[#1c1c1e] dark:text-[#f5f5f7] dark:shadow-[0_10px_32px_rgba(0,0,0,0.4)] dark:ring-white/10"
      >
        {CRUMB_PATH.map((crumb, i) => {
          const Icon = crumb.icon;
          const current = i === depth;
          const ahead = i > depth;
          const named = current || (i < depth && i >= depth - visible) || peek === i;
          return (
            <motion.li
              key={crumb.label}
              layout
              transition={CRUMB_SPRING}
              className="flex min-w-0 items-center"
            >
              {i > 0 && (
                <span
                  aria-hidden
                  className="select-none px-0.5 text-[16px] font-light text-black/15 dark:text-white/20"
                >
                  /
                </span>
              )}
              <button
                type="button"
                onClick={() => setDepth(i)}
                onPointerEnter={() => setPeek(i)}
                onFocus={() => setPeek(i)}
                onBlur={() => setPeek(null)}
                aria-current={current ? "page" : undefined}
                aria-label={ahead ? "Revenir à " + crumb.label : crumb.label}
                className={
                  "group flex h-9 min-w-9 items-center justify-center rounded-full px-2 outline-none transition-[background-color,color,opacity] duration-200 focus-visible:ring-2 active:scale-[0.96] " +
                  (current
                    ? "font-medium"
                    : "text-[#86868b] hover:bg-black/[0.04] hover:text-[#1d1d1f] dark:hover:bg-white/[0.07] dark:hover:text-[#f5f5f7]") +
                  (ahead ? " opacity-40 hover:opacity-100 focus-visible:opacity-100" : "")
                }
                style={{ ["--tw-ring-color" as string]: accent + "66" }}
              >
                <Icon
                  className="h-4 w-4 shrink-0"
                  strokeWidth={1.5}
                  style={{ color: current ? accent : undefined }}
                />
                <AnimatePresence initial={false}>
                  {named && (
                    <motion.span
                      key="label"
                      initial={{ width: 0, opacity: 0, filter: "blur(3px)" }}
                      animate={{ width: "auto", opacity: 1, filter: "blur(0px)" }}
                      exit={{ width: 0, opacity: 0, filter: "blur(3px)" }}
                      transition={CRUMB_SPRING}
                      className="block overflow-hidden whitespace-nowrap"
                    >
                      <span className="block max-w-[8.5rem] truncate pl-1.5">{crumb.label}</span>
                    </motion.span>
                  )}
                </AnimatePresence>
              </button>
            </motion.li>
          );
        })}
      </motion.ol>
    </nav>
  );
}

Prompt pour l’IA

Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.

Plus dans Navigation

Où l’utiliser

Les chemins profonds prennent vite de la place : un gestionnaire de fichiers, un centre d’aide avec des rubriques imbriquées, une catégorie comme Accueil / Femme / Chaussures / Baskets. Ce fil d’Ariane en React ne nomme que la page actuelle et garde le reste en icônes. Quand vous remontez, les niveaux quittés restent estompés pour y revenir.

Sur écran tactile il n’y a pas de survol, les noms n’apparaissent qu’au toucher. Si vos icônes ne sont pas parlantes, passez « Parents avec nom » à un ou deux. Et choisissez des icônes connues de tous : une maison pour l’accueil, un dossier pour les projets.