Dynamic breadcrumbs in Next.js

4 min read

tl;dr - Implementing dynamic breadcrumbs in Next.js using the app router.


  • Map the current path to breadcrumbs
  • Utilize context to set breadcrumbs asynchronously
  • Full code on github
  • Checkout the breadcrumbs in this demo

Often, breadcrumbs are used to show the user’s current location within the site hierarchy. In a static site, breadcrumbs are typically hard-coded into the page. However, in a dynamic site, breadcrumbs need to be generated based on the current path. Additionally, sometimes one needs to set breadcrumbs asynchronously, such as when fetching data from an API. In this post, I’ll demonstrate one of many ways to implement dynamic breadcrumbs in Next.js when using the app router.



Getting started


We start by creating a new Next.js project with npx create-next-app which we will use for demonstration. For breadcrumbs to add value, we need to have multiple pages, ideally, with some navigational depth. Without going into into too much detail, we will simply add a few pages representing categories of pets. The possible navigation paths look like this:

Home -> Category -> Variant -> Example pet



We can now proceed to add breadcrumbs to our project starting with the UI part of the breadcrumbs. We will create two components, BreadcrumbsContainer and BreadcrumbsItem. The BreadcrumbsContainer wraps each BreadcrumbsItem which represents a single part of the current path while the BreadcrumbsContainer is responsible for rendering the separator between each BreadcrumbsItem.

const BreadcrumbsItem = ({
  children,
  href,
  ...props
}: BreadcrumbItemProps) => {
  return (
    <li {...props}>
      <Link href={href} passHref>
        {children}
      </Link>
    </li>
  );
};

const BreadcrumbsContainer = ({
  children,
  separator = '/',
}: BreadcrumbsContainerProps) => (
  <nav className="min-h-6 pb-6">
    <ol className="flex items-center space-x-4">
      {Children.map(children, (child, index) => (
        <>
          {child}
          {index < Children.count(children) - 1
            ? <span>{separator}</span>
            : null}
        </>
      ))}
    </ol>
  </nav>
);

With these components we can already create a simple dynamic breadcrumb component which covers many use cases:

import { Children } from 'react';
import { usePathname } from 'next/navigation';

const BreadCrumbs = ({
  children,
}: BreadcrumbsProps) => {
  const paths = usePathname();
  const pathNames = paths.split('/').filter((path) => path);
  const pathItems = pathNames
    .map((path, i) => ({
      // Optionally you can capitalize the first letter here
      name: path,
      path: pathNames.slice(0, i + 1).join('/'),
    }));

  return (
    <BreadcrumbsContainer>
      {pathItems.map((item) => (
        <BreadcrumbsItem key={item.path} href={`/${item.path}`}>
          {item.name}
        </BreadcrumbsItem>
      ))}
    </BreadcrumbsContainer>
  );
};

This component will render a list of breadcrumbs based on the current path. It is great for sites where each path segment has a meaningful name. However, in many cases, the path segments are just IDs or slugs without accessible meaning for the end user. It would be desirable to set the breadcrumbs independent from the current path. Here we will focus only on setting the last item of the breadcrumbs, though the concept can be extended to set all parts of the breadcrumbs.




Extending on the previous example, we can add context to set the breadcrumbs component. This will enable us to set the last breadcrumbs item to a value of our choice.

const BreadCrumbsContext = createContext<Context>({
  trailingPath: '',
  setTrailingPath: () => {},
});

const BreadCrumbs = ({
  children,
}: BreadcrumbsProps) => {
  const paths = usePathname();
  const [trailingPath, setTrailingPath] = useState('');
  const context = useMemo(() => ({
    trailingPath,
    setTrailingPath,
  }), [trailingPath]);

  const pathNames = paths.split('/').filter((path) => path);
  const pathItems = pathNames
    .map((path, i) => ({
      name: path,
      path: pathNames.slice(0, i + 1).join('/'),
    }));

  if (trailingPath && pathItems.length > 0 && trailingPath !== pathItems[pathItems.length - 1].name) {
    pathItems[pathItems.length - 1].name = trailingPath;
  }

  return (
    <>
      <BreadcrumbsContainer>
        {pathItems.map((item) => (
          <BreadcrumbsItem key={item.path} href={`/${item.path}`}>
            {item.name === 'loading'
              ? <LoadingSpinner className="w-4 h-4" />
              : item.name}
          </BreadcrumbsItem>
        ))}
      </BreadcrumbsContainer>
      <BreadCrumbsContext.Provider value={context}>
        {children}
      </BreadCrumbsContext.Provider>
    </>
  );
};

The only changes made are the addition of the trailingPath state and the BreadCrumbsContext.Provider which will wrap the page content. If a trailing path is set, it will replace the last item in the breadcrumbs giving us the ability to set the last breadcrumb to a arbitrary value. To actually set the last breadcrumb we introduce the helper useBreadCrumbs which uses the breadcrumb context and sets the trailingPath for us.

export const useBreadCrumbs = (trailingPath?: string) => {
  const context = useContext(BreadCrumbsContext);

  useEffect(() => {
    context.setTrailingPath(trailingPath ? trailingPath : 'loading');
    return () => context.setTrailingPath('');
  }, [trailingPath, context]);
}


Using the Breadcrumbs component


To add breadcrumbs to our site we wrap the child elements in the layout.tsx file

// ...
<BreadCrumbs>
  {children}
</BreadCrumbs>
// ...

We can now set the last breadcrumb using the useBreadCrumbs hook from any page.

const Page = () => {
  useBreadCrumbs('Example pet');

  return (
    <div>
      <h1>Example pet</h1>
    </div>
  );
};

This also works with asynchronous data fetching. For example, we can fetch the pet name from an API and set the breadcrumb when the data is available. It is straightforward to add a loading state while the asynchronous data is being fetched. A full example with some twerks can be found here.