React + MotionNavegación

Breadcrumb que se colapsa

Las migas se quedan en icono y despliegan su nombre con un muelle al pasar o enfocar.

Ajustes

0

Código

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: "Inicio", icon: House },
  { label: "Proyectos", icon: Folder },
  { label: "Aimran", icon: Box },
  { label: "Diseño", icon: Palette },
  { label: "Navegación", 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="Ruta" 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 ? "Volver a " + 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 para IA

Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.

Más en Navegación

Dónde usarlo

Las rutas largas se comen el espacio: un gestor de archivos, un centro de ayuda con secciones dentro de secciones, una categoría de tienda como Inicio / Mujer / Calzado / Zapatillas. Estas migas de pan en React dejan con nombre solo la página actual y el resto en icono. Al subir, los niveles que dejas atrás quedan en fantasma para volver.

En pantallas táctiles no hay hover, así que los nombres solo aparecen al tocar. Si tus iconos no se entienden solos, sube «Padres con nombre» a uno o dos. Y usa iconos que la gente ya conoce: una casa para Inicio, una carpeta para proyectos.