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