React + MotionNavigation

Collapsing breadcrumb

Breadcrumbs shrink to icons and spring open to show their name on hover or focus, keeping deeper levels ghosted.

Settings

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: "Home", icon: House },
  { label: "Projects", 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="Breadcrumb" 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 ? "Back to " + 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 for AI

Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.

More in Navigation

Where to use it

Deep paths eat space fast: a file manager, a help centre with sections inside sections, a shop category like Home / Women / Shoes / Trainers. This breadcrumb in React keeps only the current page named and the rest as icons. When you jump back up, the levels you left stay faded so you can return to them.

On touch screens there is no hover, so the names only show when tapped. If your icons are not obvious, raise “parents with names” to one or two. And choose icons people already know, a house for Home, a folder for projects.