What we’re going to build today
I'm currently working on the Floww application, an AI agent blockchain wallet.
You can read the linked article to learn more about the project.
Click here to read the first part!
After long days of building the API connections and authentication, I've finally moved on to the dashboard.
You've probably seen those tiny menus at the top of websites. They're called breadcrumbs.
Just like the name suggests, they leave a trail you can follow, so you never get lost on the website.
So let's get started!
Folder Structure
How you build breadcrumbs depends on how your project is structured.
In my case, every page has its own route, even the subpages. That made things pretty simple.

(home) is a route group, so it doesn't show up in the URL.
Since the URL already matches the page structure, I can build the trail just by reading the current path.
How I wanted it
I want the breadcrumb to show each subpage alongside its main page. But there's a RULE.
On a main page, show the icon and the label.

On a subpage, the main page shrinks to an icon only, and the rest show their labels.
🏠 > My Wallet > TransactionsThis keeps the top bar short, even when the trail gets longer.
Menus & SubMenus
I split the data into two lists.
menus holds the main pages. Each one has a label, a link, and an icon.
const menus = [
{ label: "Floww Agent", href: "/app/chat", icon: <svg>...</svg> },
{ label: "Home", href: "/app", icon: <svg>...</svg> },
{ label: "Create Mandate", href: "/app/mandate", icon: <svg>...</svg> },
// Activity, Connect, Profile, Settings
];submenus holds the subpages. They don't need icons, but they do need a parentHref.
This tells the component which page comes before them.
const submenus = [
{ label: "My Wallet", href: "/app/my-wallet", parentHref: "/app" },
{ label: "Transactions", href: "/app/my-wallet/transactions", parentHref: "/app/my-wallet" },
];
const allMenus = [...menus, ...submenus];Then I merge both lists into allMenus so I can search them in one place.
Variables
const pathname = usePathname();
const currentPathname = pathname === "/app/home" ? "/app" : pathname;
const crumbs = [];
let current = allMenus.find((menu) => menu.href === currentPathname);
const visited = new Set();pathname is the current URL, from Next.js usePathname.currentPathname treats /app/home the same as /app, so Home always matches.crumbs is the trail we'll render.current is the menu that matches the current page.visited keeps track of the pages we already added.
Now let's build the trail.
while (current && !visited.has(current.href)) {
visited.add(current.href);
crumbs.unshift(current);
current = current.parentHref
? allMenus.find((menu) => menu.href === current.parentHref)
: null;
}We start from the current page and walk up through each parentHref.unshift adds each page to the front, so the trail ends up in the right order.
On /app/my-wallet/transactions, it works like this:
Transactions → My Wallet → Home
crumbs = [Home, My Wallet, Transactions]The visited set is a safety net. If two pages ever point to each other by mistake, the loop stops instead of running forever.
If no menu matches, we render nothing.
if (!crumbs.length) return null;Semantic HTML
Breadcrumbs are navigation, so I used real navigation elements.
<nav aria-label="Breadcrumb">
<ol>
<li>...</li>
</ol>
</nav><nav> with aria-label="Breadcrumb" tells screen readers what this menu is.<ol> is used because the order matters.aria-current="page" marks the page you're on.
The > separator has aria-hidden="true", so screen readers skip it.
Rendering Logic
{crumbs.map((crumb, index) => {
const isLast = index === crumbs.length - 1;
const iconOnly = index === 0 && crumbs.length > 1;
...
})}Two variables decide how each crumb looks.
isLast is the current page. It's plain text, not a link, because you're already there.iconOnly is true for the first crumb when there's more than one. This is where the RULE lives.
{isLast ? (
<span aria-current="page">
{crumbs.length === 1 && crumb.icon}
{crumb.label}
</span>
) : (
<Link href={crumb.href} aria-label={iconOnly ? crumb.label : undefined}>
{iconOnly ? crumb.icon : crumb.label}
</Link>
)}When there's only one crumb, it shows the icon with the label.
When the first crumb is icon only, it gets an aria-label, so screen readers still read "Home".
Full Code
Icons are shortened to <svg>...</svg> to keep it readable.
You can also visit my repository to see the full code.
"use client"
import Link from "next/link";
import { usePathname } from "next/navigation";
const menus = [
{ label: "Floww Agent", href: "/app/chat", icon: <svg>...</svg> },
{ label: "Home", href: "/app", icon: <svg>...</svg> },
{ label: "Create Mandate", href: "/app/mandate", icon: <svg>...</svg> },
{ label: "Activity", href: "/app/activity", icon: <svg>...</svg> },
{ label: "Connect", href: "/app/connect", icon: <svg>...</svg> },
{ label: "Profile", href: "/app/profile", icon: <svg>...</svg> },
{ label: "Settings", href: "/app/settings", icon: <svg>...</svg> },
];
const submenus = [
{ label: "My Wallet", href: "/app/my-wallet", parentHref: "/app" },
{ label: "Transactions", href: "/app/my-wallet/transactions", parentHref: "/app/my-wallet" },
];
const allMenus = [...menus, ...submenus];
const BreadCrumbs = () => {
const pathname = usePathname();
const currentPathname = pathname === "/app/home" ? "/app" : pathname;
const crumbs = [];
let current = allMenus.find((menu) => menu.href === currentPathname);
const visited = new Set();
while (current && !visited.has(current.href)) {
visited.add(current.href);
crumbs.unshift(current);
current = current.parentHref
? allMenus.find((menu) => menu.href === current.parentHref)
: null;
}
if (!crumbs.length) return null;
return (
<nav aria-label="Breadcrumb" className="w-auto h-auto flex items-center">
<ol className="flex items-center gap-2 text-sm text-text-muted">
{crumbs.map((crumb, index) => {
const isLast = index === crumbs.length - 1;
const iconOnly = index === 0 && crumbs.length > 1;
return (
<li key={crumb.href} className="flex items-center gap-2">
{index > 0 && <span aria-hidden="true">></span>}
{isLast ? (
<span aria-current="page" className="flex items-center gap-2">
{crumbs.length === 1 && crumb.icon}
{crumb.label}
</span>
) : (
<Link href={crumb.href} aria-label={iconOnly ? crumb.label : undefined}
className="flex items-center rounded focus-visible:outline-2">
{iconOnly ? crumb.icon : crumb.label}
</Link>
)}
</li>
);
})}
</ol>
</nav>
);
};
export default BreadCrumbs;Finally, I placed it in the TopNav right next to the logo.
<div className="flex flex-row gap-6">
<Link href="/app">
<AuthBrand />
</Link>
<BreadCrumbs />
</div>Now, whenever I add a new subpage, I just add one line to submenus with its parentHref, and the trail builds itself.

