Breadcrumb
Uma trilha horizontal de links mostrando a localização atual do usuário na hierarquia de navegação, permitindo um rápido retorno às páginas pai.
Busque em todas as páginas da documentação
Uma trilha horizontal de links mostrando a localização atual do usuário na hierarquia de navegação, permitindo um rápido retorno às páginas pai.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
"use client";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
export function Breadcrumb({ items }: BreadcrumbProps) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-2 text-sm text-gray-500">
{items.map((item, index) => (
<li key={index} className="flex items-center gap-2">
{index > 0 && <span aria-hidden="true">/</span>}
{item.href ? (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="font-medium text-gray-900">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}O último item não tem href, renderizando-o como texto simples para indicar a página atual. O separador usa aria-hidden para que os leitores de tela o ignorem e confiem na semântica do <ol> em vez disso.
"use client";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
export function Breadcrumb({ items }: BreadcrumbProps) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{items.map((item, index) => (
<li key={index} className="flex items-center gap-1.5">
{index > 0 && (
<svg
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
{item.href ? (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="font-medium text-gray-900">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}Substitui o separador de barra por um SVG de chevrão inline. O chevrão é aria-hidden, pois a lista ordenada já transmite a hierarquia para tecnologias assistivas.
"use client";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
icon?: React.ReactNode;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
export function Breadcrumb({ items }: BreadcrumbProps) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{items.map((item, index) => (
<li key={index} className="flex items-center gap-1.5">
{index > 0 && (
<svg
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
{item.href ? (
<Link href={item.href} className="flex items-center gap-1.5 hover:text-gray-900">
{item.icon && <span className="h-4 w-4">{item.icon}</span>}
{item.label}
</Link>
) : (
<span className="flex items-center gap-1.5 font-medium text-gray-900">
{item.icon && <span className="h-4 w-4">{item.icon}</span>}
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>
);
}Cada item do breadcrumb pode opcionalmente exibir um ícone à esquerda de seu rótulo. Isso é especialmente útil para o item "Home", que é frequentemente representado por um ícone de casa em vez de texto.
"use client";
import { useState } from "react";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
maxVisible?: number;
}
export function Breadcrumb({ items, maxVisible = 3 }: BreadcrumbProps) {
const [expanded, setExpanded] = useState(false);
const shouldTruncate = items.length > maxVisible && !expanded;
const visibleItems = shouldTruncate
? [items[0], ...items.slice(-(maxVisible - 1))]
: items;
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{visibleItems.map((item, index) => (
<li key={index} className="flex items-center gap-1.5">
{index > 0 && <span className="text-gray-400" aria-hidden="true">/</span>}
{/* Botão de elipse inserido após o primeiro item */}
{shouldTruncate && index === 1 && (
<>
<button
onClick={() => setExpanded(true)}
className="rounded px-1 py-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
aria-label="Mostrar caminho completo do breadcrumb"
>
...
</button>
<span className="text-gray-400" aria-hidden="true">/</span>
</>
)}
{item.href ? (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="font-medium text-gray-900">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}Quando a trilha do breadcrumb excede maxVisible itens, os itens do meio colapsam em uma elipse clicável. Clicar nela revela o caminho completo. Os primeiro e último itens permanecem sempre visíveis para contexto.
"use client";
import { useState, useRef, useEffect } from "react";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
maxVisible?: number;
}
export function Breadcrumb({ items, maxVisible = 3 }: BreadcrumbProps) {
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const shouldTruncate = items.length > maxVisible;
const hiddenItems = shouldTruncate ? items.slice(1, -(maxVisible - 1)) : [];
const visibleItems = shouldTruncate
? [items[0], ...items.slice(-(maxVisible - 1))]
: items;
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setDropdownOpen(false);
}
}
if (dropdownOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [dropdownOpen]);
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{visibleItems.map((item, index) => (
<li key={index} className="flex items-center gap-1.5">
{index > 0 && <span className="text-gray-400" aria-hidden="true">/</span>}
{shouldTruncate && index === 1 && (
<>
<div ref={dropdownRef} className="relative">
<button
onClick={() => setDropdownOpen((prev) => !prev)}
className="rounded px-1.5 py-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
aria-label="Mostrar itens ocultos do breadcrumb"
aria-expanded={dropdownOpen}
>
...
</button>
{dropdownOpen && (
<div className="absolute left-0 top-full z-10 mt-1 min-w-[160px] rounded-lg border bg-white py-1 shadow-lg">
{hiddenItems.map((hidden, hIdx) => (
<Link
key={hIdx}
href={hidden.href ?? "#"}
className="block px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => setDropdownOpen(false)}
>
{hidden.label}
</Link>
))}
</div>
)}
</div>
<span className="text-gray-400" aria-hidden="true">/</span>
</>
)}
{item.href ? (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="font-medium text-gray-900">{item.label}</span>
)}
</li>
))}
</ol>
</nav>
);
}Em vez de expandir inline, os itens ocultos aparecem em um menu suspenso. O menu suspenso fecha ao clicar fora. Isso é preferível quando a trilha completa do breadcrumb é muito longa e quebraria o layout.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface BreadcrumbOverride {
[segment: string]: string;
}
interface DynamicBreadcrumbProps {
overrides?: BreadcrumbOverride;
homeLabel?: string;
}
export function DynamicBreadcrumb({ overrides = {}, homeLabel = "Home" }: DynamicBreadcrumbProps) {
const pathname = usePathname();
const segments = pathname.split("/").filter(Boolean);
const items = segments.map((segment, index) => {
const href = "/" + segments.slice(0, index + 1).join("/");
const label = overrides[segment] ?? segment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
return { label, href };
});
const all = [{ label: homeLabel, href: "/" }, ...items];
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{all.map((item, index) => (
<li key={item.href} className="flex items-center gap-1.5">
{index > 0 && <span className="text-gray-400" aria-hidden="true">/</span>}
{index === all.length - 1 ? (
<span className="font-medium text-gray-900">{item.label}</span>
) : (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
)}
</li>
))}
</ol>
</nav>
);
}
// Usage
// <DynamicBreadcrumb overrides={{ "user-settings": "Settings", "123": "John Doe" }} />Constrói automaticamente a trilha do breadcrumb a partir do caminho da URL atual. Os segmentos são em maiúsculas por padrão, mas o mapa overrides permite fornecer rótulos legíveis para slugs e IDs dinâmicos.
"use client";
import { createContext, useContext, useMemo, useState, useRef, useEffect, useCallback } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
// --- Tipos ---
interface BreadcrumbItem {
label: string;
href?: string;
icon?: React.ReactNode;
}
interface BreadcrumbContextValue {
items: BreadcrumbItem[];
setItems: (items: BreadcrumbItem[]) => void;
}
// --- Contexto ---
const BreadcrumbContext = createContext<BreadcrumbContextValue | null>(null);
export function BreadcrumbProvider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<BreadcrumbItem[]>([]);
return (
<BreadcrumbContext.Provider value={{ items, setItems }}>
{children}
</BreadcrumbContext.Provider>
);
}
export function useBreadcrumb() {
const ctx = useContext(BreadcrumbContext);
if (!ctx) throw new Error("useBreadcrumb must be used inside BreadcrumbProvider");
return ctx;
}
// --- Hook para definir breadcrumbs de uma página ---
export function useSetBreadcrumbs(items: BreadcrumbItem[]) {
const { setItems } = useBreadcrumb();
const serialized = JSON.stringify(items);
useEffect(() => {
setItems(JSON.parse(serialized));
}, [serialized, setItems]);
}
// --- Separador ---
function Separator() {
return (
<svg
className="h-4 w-4 flex-shrink-0 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
);
}
// --- Dropdown para itens ocultos ---
interface EllipsisDropdownProps {
items: BreadcrumbItem[];
}
function EllipsisDropdown({ items }: EllipsisDropdownProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [open]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
},
[]
);
return (
<div ref={ref} className="relative" onKeyDown={handleKeyDown}>
<button
onClick={() => setOpen((prev) => !prev)}
className="rounded px-1.5 py-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
aria-label="Mostrar itens ocultos do breadcrumb"
aria-expanded={open}
aria-haspopup="true"
>
...
</button>
{open && (
<div
role="menu"
className="absolute left-0 top-full z-20 mt-1 min-w-[180px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
>
{items.map((item, idx) => (
<Link
key={idx}
href={item.href ?? "#"}
role="menuitem"
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => setOpen(false)}
>
{item.icon && <span className="h-4 w-4 flex-shrink-0">{item.icon}</span>}
{item.label}
</Link>
))}
</div>
)}
</div>
);
}
// --- Componente Breadcrumb ---
interface BreadcrumbProps {
items?: BreadcrumbItem[];
maxVisible?: number;
separator?: React.ReactNode;
className?: string;
}
export function Breadcrumb({ items: propItems, maxVisible = 4, separator, className }: BreadcrumbProps) {
const ctx = useContext(BreadcrumbContext);
const items = propItems ?? ctx?.items ?? [];
const { visible, hidden, shouldTruncate } = useMemo(() => {
if (items.length <= maxVisible) {
return { visible: items, hidden: [], shouldTruncate: false };
}
return {
visible: [items[0], ...items.slice(-(maxVisible - 1))],
hidden: items.slice(1, -(maxVisible - 1)),
shouldTruncate: true,
};
}, [items, maxVisible]);
if (items.length === 0) return null;
const sep = separator ?? <Separator />;
return (
<nav aria-label="Breadcrumb" className={className}>
<ol className="flex items-center gap-1.5 text-sm text-gray-500">
{visible.map((item, index) => {
const isLast = index === visible.length - 1;
return (
<li key={index} className="flex items-center gap-1.5">
{index > 0 && sep}
{shouldTruncate && index === 1 && (
<>
<EllipsisDropdown items={hidden} />
{sep}
</>
)}
{isLast || !item.href ? (
<span
className={`flex items-center gap-1.5 ${
isLast ? "font-medium text-gray-900" : ""
}`}
aria-current={isLast ? "page" : undefined}
>
{item.icon && <span className="h-4 w-4 flex-shrink-0">{item.icon}</span>}
<span className="max-w-[200px] truncate">{item.label}</span>
</span>
) : (
<Link
href={item.href}
className="flex items-center gap-1.5 hover:text-gray-900"
>
{item.icon && <span className="h-4 w-4 flex-shrink-0">{item.icon}</span>}
<span className="max-w-[200px] truncate">{item.label}</span>
</Link>
)}
</li>
);
})}
</ol>
</nav>
);
}Aspectos chave:
BreadcrumbProvider, permitindo que as páginas definam declarativamente sua trilha de breadcrumb via useSetBreadcrumbs.maxVisible, os itens do meio colapsam em um botão de elipse que revela um menu suspenso com papéis ARIA corretos (menu, menuitem).max-w-[200px] truncate para evitar que títulos de página extremamente longos quebrem o layout.aria-current="page" - o último item é marcado com aria-current="page" para que os leitores de tela o anunciem como a página atual, seguindo as práticas de autoria de breadcrumb da WAI-ARIA.separator aceita qualquer ReactNode, com padrão para um SVG de chevrão. Isso permite personalização fácil para barras, setas ou outros divisores.aria-expanded e aria-haspopup corretos.useSetBreadcrumbs serializa o array de itens para JSON para a dependência do efeito, evitando loops de re-renderização infinitos de novas referências de array.Usar <ul> em vez de <ol> - Breadcrumbs representam uma sequência ordenada. Leitores de tela anunciam a posição (por exemplo, "item 2 de 5") com <ol>, o que é perdido com <ul>.
Separadores decorativos lidos por leitores de tela - Se os separadores estiverem dentro dos itens da lista e não marcados como aria-hidden="true", os leitores de tela os anunciarão (por exemplo, "barra" ou "maior que").
Tornar a página atual um link - O último item do breadcrumb não deve ser um link, pois representa a página atual. Vinculá-lo cria uma navegação autorreferencial confusa.
Não usar aria-label="Breadcrumb" na nav - Sem um rótulo, os leitores de tela não conseguem distinguir a navegação do breadcrumb de outros elementos <nav> na página.
Breadcrumbs dinâmicos causando mudança de layout - Se os itens do breadcrumb carregarem assincronamente, o componente pode saltar de altura. Reserve espaço ou use um esqueleto para evitar layout shift cumulativo.
Truncagem escondendo contexto importante - Colapsar agressivamente demais (por exemplo, maxVisible={2}) pode ocultar a parte mais significativa do caminho. Escolha um limite que mantenha contexto suficiente visível.
Esquecer aria-current="page" no último item - Este atributo informa às tecnologias assistivas qual item representa a página atual. Sem ele, o breadcrumb é menos útil para usuários de leitores de tela.
Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥