Retour au catalogue

Breadcrumb Animated Trail

Fil d'Ariane avec effet de trainee lumineuse animee entre chaque element, particules flottantes sur le chemin actif.

breadcrumbmedium Both Responsive a11y
playfulboldsaasagencystacked
Theme
"use client";

import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Home, ChevronRight, Sparkles } from "lucide-react";
import React from "react";

interface BreadcrumbItem {
  label: string;
  href?: string;
  icon?: "home" | "sparkles";
}

interface BreadcrumbAnimatedTrailProps {
  items?: BreadcrumbItem[];
}

const iconMap: Record<string, React.ElementType> = {
  home: Home,
  sparkles: Sparkles,
};

function TrailLine({ index }: { index: number }) {
  return (
    <motion.div
      initial={{ scaleX: 0, opacity: 0 }}
      animate={{ scaleX: 1, opacity: 1 }}
      transition={{ delay: index * 0.15 + 0.1, duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
      style={{
        width: "24px",
        height: "2px",
        background: "linear-gradient(90deg, var(--color-accent), color-mix(in srgb, var(--color-accent) 30%, transparent))",
        borderRadius: "1px",
        transformOrigin: "left center",
        position: "relative",
        overflow: "visible",
      }}
    >
      <motion.div
        animate={{ x: [0, 24, 0] }}
        transition={{ duration: 2, repeat: Infinity, ease: "linear", delay: index * 0.3 }}
        style={{
          position: "absolute",
          top: "-2px",
          left: 0,
          width: "6px",
          height: "6px",
          borderRadius: "50%",
          background: "var(--color-accent)",
          opacity: 0.6,
          filter: "blur(1px)",
        }}
      />
    </motion.div>
  );
}

export default function BreadcrumbAnimatedTrail({
  items,
}: BreadcrumbAnimatedTrailProps) {
  const [mounted, setMounted] = useState(false);

  const defaultItems: BreadcrumbItem[] = [
    { label: "Accueil", href: "#", icon: "home" },
    { label: "Catalogue", href: "#" },
    { label: "Design", href: "#" },
    { label: "Typographie" },
  ];

  const resolvedItems = items ?? defaultItems;

  useEffect(() => {
    setMounted(true);
  }, []);

  return (
    <nav
      aria-label="Fil d'Ariane"
      style={{
        padding: "1rem 1.5rem",
        background: "var(--color-background)",
      }}
    >
      <ol
        style={{
          display: "flex",
          alignItems: "center",
          gap: "0",
          listStyle: "none",
          margin: 0,
          padding: 0,
          fontSize: "0.875rem",
        }}
      >
        <AnimatePresence>
          {resolvedItems.map((item, i) => {
            const isLast = i === resolvedItems.length - 1;
            const Icon = item.icon ? iconMap[item.icon] : null;

            return (
              <motion.li
                key={`${item.label}-${i}`}
                initial={{ opacity: 0, x: -16, filter: "blur(4px)" }}
                animate={mounted ? { opacity: 1, x: 0, filter: "blur(0px)" } : {}}
                transition={{ delay: i * 0.15, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: "0",
                }}
              >
                {i > 0 && <TrailLine index={i} />}

                {isLast ? (
                  <motion.span
                    style={{
                      fontWeight: 600,
                      color: "var(--color-foreground)",
                      position: "relative",
                      padding: "0.25rem 0.5rem",
                      borderRadius: "6px",
                      background: "color-mix(in srgb, var(--color-accent) 10%, transparent)",
                    }}
                  >
                    {item.label}
                    <motion.div
                      animate={{ opacity: [0.4, 0.8, 0.4] }}
                      transition={{ duration: 2, repeat: Infinity }}
                      style={{
                        position: "absolute",
                        inset: 0,
                        borderRadius: "inherit",
                        border: "1px solid color-mix(in srgb, var(--color-accent) 30%, transparent)",
                        pointerEvents: "none",
                      }}
                    />
                  </motion.span>
                ) : (
                  <a
                    href={item.href || "#"}
                    style={{
                      display: "inline-flex",
                      alignItems: "center",
                      gap: "0.375rem",
                      color: "var(--color-foreground-muted)",
                      textDecoration: "none",
                      padding: "0.25rem 0.5rem",
                      borderRadius: "6px",
                      transition: "color 0.2s, background 0.2s",
                    }}
                    onMouseEnter={(e) => {
                      e.currentTarget.style.color = "var(--color-accent)";
                      e.currentTarget.style.background = "color-mix(in srgb, var(--color-accent) 6%, transparent)";
                    }}
                    onMouseLeave={(e) => {
                      e.currentTarget.style.color = "var(--color-foreground-muted)";
                      e.currentTarget.style.background = "transparent";
                    }}
                  >
                    {Icon && <Icon style={{ width: 14, height: 14 }} />}
                    {item.label}
                  </a>
                )}
              </motion.li>
            );
          })}
        </AnimatePresence>
      </ol>
    </nav>
  );
}

Avis

Breadcrumb Animated Trail — React Breadcrumb Section — Incubator