Web InnoventixFreeCode

Basic Breadcrumb

Original · free

basic breadcrumb trail

byWeb InnoventixReact + Tailwind
crumbbasicbreadcrumbs
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/crumb-basic.json
crumb-basic.tsx
"use client";

import { useId, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Crumb = {
  label: string;
  href: string;
};

type Trail = {
  id: string;
  name: string;
  description: string;
  crumbs: Crumb[];
};

const TRAILS: Trail[] = [
  {
    id: "docs",
    name: "Documentation",
    description:
      "A compact navigation trail that shows where the current page sits in the site hierarchy and lets people jump back to any ancestor level in a single click.",
    crumbs: [
      { label: "Home", href: "#/" },
      { label: "Documentation", href: "#/docs" },
      { label: "Components", href: "#/docs/components" },
      { label: "Navigation", href: "#/docs/components/navigation" },
      { label: "Breadcrumbs", href: "#/docs/components/navigation/breadcrumbs" },
    ],
  },
  {
    id: "blog",
    name: "Engineering blog",
    description:
      "How we rebuilt our primary navigation around semantic landmarks, roving focus, and visible focus states — and what the accessibility audit taught us along the way.",
    crumbs: [
      { label: "Home", href: "#/" },
      { label: "Blog", href: "#/blog" },
      { label: "Engineering", href: "#/blog/engineering" },
      { label: "Building Accessible Navigation", href: "#/blog/engineering/accessible-nav" },
    ],
  },
  {
    id: "market",
    name: "Marketplace",
    description:
      "Every release note for the Nebula admin template, from the initial 1.0 launch through the latest performance passes and refined dark-mode surfaces.",
    crumbs: [
      { label: "Home", href: "#/" },
      { label: "Marketplace", href: "#/market" },
      { label: "UI Kits", href: "#/market/ui-kits" },
      { label: "Nebula Dashboard", href: "#/market/ui-kits/nebula" },
      { label: "Changelog", href: "#/market/ui-kits/nebula/changelog" },
    ],
  },
];

function HomeIcon() {
  return (
    <svg
      viewBox="0 0 24 24"
      className="h-4 w-4 shrink-0"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M3 10.5 12 4l9 6.5" />
      <path d="M5 9.4V20h5v-5.5h4V20h5V9.4" />
    </svg>
  );
}

function ChevronSep() {
  return (
    <svg
      viewBox="0 0 24 24"
      className="mx-1 h-4 w-4 shrink-0 text-slate-300 dark:text-slate-600 sm:mx-1.5"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="m9 6 6 6-6 6" />
    </svg>
  );
}

function DotsIcon() {
  return (
    <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
      <circle cx="5" cy="12" r="1.6" />
      <circle cx="12" cy="12" r="1.6" />
      <circle cx="19" cy="12" r="1.6" />
    </svg>
  );
}

const linkClass =
  "inline-flex items-center gap-1.5 rounded-md px-1.5 py-1 text-slate-500 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";

function CrumbLink({ crumb, withIcon }: { crumb: Crumb; withIcon?: boolean }) {
  return (
    <a href={crumb.href} className={linkClass}>
      {withIcon ? <HomeIcon /> : null}
      <span>{crumb.label}</span>
    </a>
  );
}

function CurrentCrumb({ label }: { label: string }) {
  return (
    <span
      aria-current="page"
      className="inline-flex items-center rounded-md px-1.5 py-1 font-medium text-slate-900 dark:text-slate-50"
    >
      {label}
    </span>
  );
}

export default function CrumbBasic() {
  const reduce = useReducedMotion();
  const groupLabelId = useId();
  const [trailId, setTrailId] = useState<string>(TRAILS[0].id);
  const [expanded, setExpanded] = useState<boolean>(false);

  const selected = TRAILS.find((t) => t.id === trailId) ?? TRAILS[0];
  const crumbs = selected.crumbs;
  const collapsible = crumbs.length > 4;
  const head = crumbs[0];
  const middle = crumbs.slice(1, -2);
  const tail = crumbs.slice(-2);
  const currentLabel = crumbs[crumbs.length - 1].label;

  function selectTrail(id: string) {
    setTrailId(id);
    setExpanded(false);
  }

  return (
    <section className="relative w-full bg-slate-50 px-6 py-20 dark:bg-slate-950 sm:py-28">
      <style>{`
        @keyframes crumbbasic-grow {
          from { transform: scaleX(0); }
          to { transform: scaleX(1); }
        }
        .crumbbasic-underline {
          transform-origin: left center;
          animation: crumbbasic-grow 620ms cubic-bezier(0.22, 1, 0.36, 1) both;
        }
        @keyframes crumbbasic-pulse {
          0%, 100% { opacity: 1; transform: scale(1); }
          50% { opacity: 0.5; transform: scale(0.82); }
        }
        .crumbbasic-dot {
          animation: crumbbasic-pulse 2.4s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .crumbbasic-underline,
          .crumbbasic-dot {
            animation: none !important;
          }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-8">
          <nav aria-label="Breadcrumb">
            <ol className="flex flex-wrap items-center text-sm">
              {collapsible ? (
                <>
                  <li className="flex items-center">
                    <CrumbLink crumb={head} withIcon />
                  </li>

                  <li className="flex items-center">
                    <ChevronSep />
                    <button
                      type="button"
                      onClick={() => setExpanded((v) => !v)}
                      aria-expanded={expanded}
                      className={`inline-flex h-7 items-center rounded-md px-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
                        expanded
                          ? "bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-100"
                          : "text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
                      }`}
                    >
                      <DotsIcon />
                      <span className="sr-only">
                        {expanded
                          ? `Hide ${middle.length} intermediate pages`
                          : `Show ${middle.length} hidden pages`}
                      </span>
                    </button>
                  </li>

                  <AnimatePresence initial={false}>
                    {expanded
                      ? middle.map((crumb) => (
                          <motion.li
                            key={crumb.href}
                            className="flex items-center"
                            initial={reduce ? false : { opacity: 0, x: -6 }}
                            animate={reduce ? {} : { opacity: 1, x: 0 }}
                            exit={reduce ? {} : { opacity: 0, x: -6 }}
                            transition={{ duration: reduce ? 0 : 0.18, ease: "easeOut" }}
                          >
                            <ChevronSep />
                            <CrumbLink crumb={crumb} />
                          </motion.li>
                        ))
                      : null}
                  </AnimatePresence>

                  {tail.map((crumb, idx) => (
                    <li key={crumb.href} className="flex items-center">
                      <ChevronSep />
                      {idx === tail.length - 1 ? (
                        <CurrentCrumb label={crumb.label} />
                      ) : (
                        <CrumbLink crumb={crumb} />
                      )}
                    </li>
                  ))}
                </>
              ) : (
                crumbs.map((crumb, idx) => (
                  <li key={crumb.href} className="flex items-center">
                    {idx > 0 ? <ChevronSep /> : null}
                    {idx === crumbs.length - 1 ? (
                      <CurrentCrumb label={crumb.label} />
                    ) : (
                      <CrumbLink crumb={crumb} withIcon={idx === 0} />
                    )}
                  </li>
                ))
              )}
            </ol>
          </nav>

          <div className="mt-7">
            <p className="flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400">
              <span
                className="crumbbasic-dot h-1.5 w-1.5 rounded-full bg-indigo-500"
                aria-hidden="true"
              />
              Current page
            </p>
            <h1 className="mt-2 text-2xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-3xl">
              {currentLabel}
            </h1>
            <span
              key={trailId}
              className="crumbbasic-underline mt-3 block h-0.5 w-16 rounded-full bg-indigo-500"
              aria-hidden="true"
            />
            <p className="mt-4 max-w-prose text-sm leading-relaxed text-slate-600 dark:text-slate-300">
              {selected.description}
            </p>
          </div>
        </div>

        <div className="mt-6">
          <p
            id={groupLabelId}
            className="mb-2 text-xs font-medium text-slate-500 dark:text-slate-400"
          >
            Preview another path
          </p>
          <div role="group" aria-labelledby={groupLabelId} className="flex flex-wrap gap-2">
            {TRAILS.map((t) => {
              const active = t.id === trailId;
              return (
                <button
                  key={t.id}
                  type="button"
                  aria-pressed={active}
                  onClick={() => selectTrail(t.id)}
                  className={`rounded-lg px-3.5 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 ${
                    active
                      ? "bg-slate-900 text-white dark:bg-white dark:text-slate-900"
                      : "border border-slate-200 bg-white text-slate-600 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
                  }`}
                >
                  {t.name}
                </button>
              );
            })}
          </div>
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →