Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Version: 1.3.0
Last Updated: 2026-04-18
Purpose: Comprehensive guide for implementing breadcrumbs in Encore Health OS Platform
Navigation Documentation Index: For an overview of all navigation documentation, see NAVIGATION_GUIDE_INDEX.md
Overview
Breadcrumbs provide hierarchical navigation context, helping users understand their location within the application and navigate back to parent pages. Encore Health OS uses a unified breadcrumb system with:
- Desktop: Auto-generated breadcrumbs in the header (
DesktopHeader + Breadcrumbs)
- Mobile: Same trail as desktop in the fixed context row under the primary
MobileHeader row (MobileBreadcrumbs). Do not render a second duplicate trail in page bodies when the shell already shows crumbs.
- Centralized policy:
shouldShowBreadcrumbs / shouldShowMobileHeaderContextRow in src/platform/navigation/utils/breadcrumb-visibility.ts (keep ResponsiveNav main padding-top aligned with whether the context row is visible).
- Shared segment logic:
src/platform/navigation/utils/generate-route-breadcrumb-segments.ts (desktop and mobile use the same generator).
- Centralized Labels: All route labels in
route-labels.ts
Mobile placement contract
- When
shouldShowBreadcrumbs(pathname) is true, breadcrumbs appear only in MobileHeader’s second row (not inside scrollable main).
PageContainer breadcrumbs + showBreadcrumbs render only when the header policy is false for that route (avoids duplicate chrome). Prefer fixing policy/labels over in-page duplicates.
- Two-segment module routes that still need crumbs (e.g.
/ce/referral-sources, /cl/pdmp) are allowlisted in breadcrumb-visibility.ts (MODULE_TWO_SEGMENT_BREADCRUMB_ROUTES).
- Never mount
<Breadcrumbs /> inside page content; use useBreadcrumbLabel / useEntityBreadcrumb for dynamic last segments.
Quick Reference
Basic Implementation
Desktop (Auto-generated):
// Automatically included in DesktopHeader
// No code needed - works automatically based on route
Mobile:
<MobileBreadcrumbs maxSegments={2} />
Custom Route Labels:
// In route-labels.ts — keys are full paths starting with `/`
export const ROUTE_LABELS: Record<string, string> = {
'/hr/employees': 'Employees',
'/hr/employees/:id': 'Employee Details',
// ... more labels
};
Common Patterns
Adding Custom Label:
- Open
src/platform/navigation/route-labels.ts
- Add entry to
BASE_ROUTE_LABELS: '/route/path': 'Display Label' (Title Case)
- Breadcrumbs automatically use the label
npm run audit:routes-navigation enforces 100% coverage — every <Route> must have a label
Mobile Truncation:
- Default shows last 2 segments
- Adjust with
maxSegments prop
- Horizontal scroll for overflow
Component Location:
- Desktop:
@/platform/navigation/Breadcrumbs.tsx
- Mobile:
@/platform/navigation/MobileBreadcrumbs.tsx
- Labels:
@/platform/navigation/route-labels.ts
For detailed architecture and implementation, see sections below.
1. Architecture
Component Structure
src/platform/navigation/
├── Breadcrumbs.tsx # Desktop auto-generated breadcrumbs
├── MobileBreadcrumbs.tsx # Mobile-optimized breadcrumbs (same segments as desktop)
├── route-labels.ts # Centralized ROUTE_LABELS map (~150 routes)
├── MobileHeader.tsx # Integrates MobileBreadcrumbs + org switcher row
└── utils/
├── breadcrumb-visibility.ts # When header/context row shows crumbs
└── generate-route-breadcrumb-segments.ts # Shared segment builder
src/shared/ui/
└── breadcrumb.tsx # Base primitives (shadcn/ui)
src/shared/components/
└── PageContainer.tsx # Optional breadcrumbs (suppressed when header shows auto crumbs)
Data Flow
- User navigates to a route (e.g.,
/hr/employees/123/edit)
Breadcrumbs.tsx parses the pathname into segments
- Each segment is looked up in
ROUTE_LABELS
- Breadcrumb trail is rendered with links to parent routes
Breadcrumb Flow Diagram
2. Component Reference
2.1 Base Primitives (@/shared/ui/breadcrumb)
| Component | Purpose |
|---|
Breadcrumb | Container wrapper with nav semantics |
BreadcrumbList | Ordered list of breadcrumb items |
BreadcrumbItem | Individual breadcrumb segment |
BreadcrumbLink | Clickable link to parent page |
BreadcrumbPage | Current page (non-clickable) |
BreadcrumbSeparator | Visual separator (ChevronRight) |
BreadcrumbEllipsis | Collapsed segments indicator |
Automatically generates breadcrumbs based on the current route path. Used in DesktopHeader.
Features:
- Parses pathname into segments
- Looks up labels in
ROUTE_LABELS
- Handles module context
- Falls back to formatted path segments
Mobile-optimized breadcrumbs with truncation and scroll.
Props:
| Prop | Type | Default | Description |
|---|
maxSegments | number | 2 | Maximum visible segments |
Features:
- Shows last N segments
- Horizontal scroll for overflow
- 44px touch targets
- Safe area support
Centralized map of route paths to display labels. Keys are full URL paths
starting with /. The map currently contains ~1,140 entries covering every
declared <Route> in src/routes/*.tsx — npm run audit:routes-navigation
enforces this coverage and exits non-zero if any route is missing a label.
export const BASE_ROUTE_LABELS: Record<string, string> = {
// Platform
'/settings': 'Settings',
'/profile': 'My Profile',
// HR Module
'/hr': 'Workforce',
'/hr/employees': 'Employees',
'/hr/employees/:id': 'Employee Details',
// ... ~1,140 total routes
};
// Re-exported as ROUTE_LABELS for downstream consumers.
export const ROUTE_LABELS: Record<string, string> = { ...BASE_ROUTE_LABELS };
For tabbed-hub pages that pass the active tab via ?tab=, the breadcrumb
appends the tab label as the trailing segment (e.g.
HR > Recruiting > Candidates). The map supports both literal hub paths
(/hr/ats) and parameterized paths (/cl/charts/:chartId,
/pm/patients/:patientId) via single-segment wildcard matching.
export const HUB_TAB_LABELS: Record<string, Record<string, string>> = {
'/hr/ats': { dashboard: 'Dashboard', candidates: 'Candidates', /* ... */ },
'/cl/charts/:chartId': { summary: 'Summary', notes: 'Progress Notes', /* ... */ },
};
2.6 Dynamic entity labels (@/shared/lib/hooks/useEntityBreadcrumb)
The preferred way to set the trailing crumb to an entity name (e.g.
employee full name, invoice number, claim ID) on detail pages.
import { useEntityBreadcrumb } from '@/shared/lib/hooks/useEntityBreadcrumb';
const { data: employee } = useEmployeeDetail(id);
useEntityBreadcrumb(employee, (e) => e.profile?.full_name ?? 'Employee');
Under the hood this calls useBreadcrumbLabel(label), which sets the label
for the current location.pathname in BreadcrumbContext and clears it on
unmount. Both desktop and mobile breadcrumbs then render the entity name in
place of the static ROUTE_LABELS value for that path.
Coverage check: npm run audit:breadcrumb-coverage lists detail pages
that don’t yet set a dynamic label so they can be migrated. Add new
detail pages to this pattern by default.
3. Implementation Patterns
Pattern 1: Auto-Generated (Recommended)
When to use: Most pages where route structure matches breadcrumb trail.
// No explicit breadcrumbs needed - handled by DesktopHeader
export default function EmployeesPage() {
return (
<PageContainer>
<PageHeader title="Employees" />
<EmployeesList />
</PageContainer>
);
}
Result: HR > Employees
Pattern 2: Dynamic entity name via useEntityBreadcrumb (preferred for detail pages)
When to use: Detail/edit pages with a :id segment where the trailing
crumb should be the entity name (employee, invoice, patient, etc.).
import { PageContainer } from '@/shared/components/PageContainer';
import { useEntityBreadcrumb } from '@/shared/lib/hooks/useEntityBreadcrumb';
export default function DocumentDetailPage() {
const { data: document } = useDocument(id);
useEntityBreadcrumb(document, (d) => d.title);
return (
<PageContainer>
<h1>{document?.title}</h1>
<DocumentViewer document={document} />
</PageContainer>
);
}
Result: Documents > Meeting Notes 2025-01-15
Recommendation: the page H1 should include the same identifier as the
breadcrumb tail. On mobile the breadcrumb truncates to the last 2 segments,
so a generic H1 (“Document Details”) leaves the user with no entity context
besides the truncated crumb.
Pattern 3: Explicit PageContainer breadcrumbs (fallback)
When to use: Pages that need a custom trail not derivable from the route
(e.g. wizards, multi-step flows, two-segment pages outside the auto-show
policy). For most routes the header already auto-renders the trail and
PageContainer suppresses the inline copy — so prefer Pattern 1 or 2.
<PageContainer
breadcrumbs={[
{ label: 'Documents', href: '/documents' },
{ label: document?.title ?? 'Loading...' },
]}
showBreadcrumbs
>
...
</PageContainer>
⛔ Anti-pattern: importing breadcrumb primitives directly in pages
Do not import @/shared/ui/breadcrumb primitives in core or feature
pages. The shadcn primitives are reserved for PageContainer,
Breadcrumbs.tsx, and MobileBreadcrumbs.tsx. Pages should only ever rely
on:
- The auto-generated header trail (Pattern 1)
useEntityBreadcrumb / useBreadcrumbLabel for dynamic entity names (Pattern 2)
PageContainer breadcrumbs={[…]} showBreadcrumbs for custom trails (Pattern 3)
4. Adding New Routes
When creating new features, add route labels to BASE_ROUTE_LABELS in
route-labels.ts. Keys are full URL paths starting with /; values are
Title Case.
// src/platform/navigation/route-labels.ts
const BASE_ROUTE_LABELS: Record<string, string> = {
// ... existing labels
// Add your new routes
'/my-feature': 'My Feature',
'/my-feature/:id': 'Feature Details',
'/my-feature/sub-page': 'Sub Page',
};
Naming Conventions
| ✅ Do | ❌ Don’t |
|---|
'/hr/employees': 'Employees' | '/hr/employees': 'Employee Management' |
'/fa/work-orders': 'Work Orders' | '/fa/work-orders': 'work orders' |
'/hr/employees/new': 'New Employee' | '/hr/employees/new': 'Create New Employee Form' |
Fallback Behavior
If a route is not in ROUTE_LABELS, the system:
- Takes the path segment (e.g.,
work-orders)
- Replaces hyphens with spaces
- Capitalizes the first letter of each word
- Result: “Work Orders”
Always add an explicit label rather than relying on the fallback —
audit:routes-navigation enforces 100% coverage.
Two-segment leaf pages (e.g. /ce/referral-sources)
By default shouldShowBreadcrumbs hides crumbs on two-segment module
routes (/{core}/{page}), since the ModuleSwitcher already conveys
module context. If a particular two-segment leaf page still benefits from
the trail (e.g. /ce/referral-sources, /cl/pdmp), add it to
MODULE_TWO_SEGMENT_BREADCRUMB_ROUTES in
src/platform/navigation/utils/breadcrumb-visibility.ts.
5. Mobile Considerations
MobileBreadcrumbs Integration
Mobile breadcrumbs are automatically included in MobileHeader:
// src/platform/navigation/MobileHeader.tsx
<header>
{/* Logo, menu, notifications */}
</header>
<div className="border-b bg-background">
<MobileBreadcrumbs maxSegments={2} />
</div>
Truncation Behavior
| Segments | Display |
|---|
| 1 | Home |
| 2 | Parent > Current |
| 3+ | … > Parent > Current |
Touch Targets
All breadcrumb links MUST have:
- Minimum 44x44px touch area
- Adequate spacing (8px minimum between items)
- Visual feedback on touch
6. Accessibility
Required Attributes
<nav aria-label="Breadcrumb"> on container
<ol> for breadcrumb list (semantic ordering)
aria-current="page" on current page item
Keyboard Navigation
- All links must be focusable
- Tab order follows visual order
- Focus indicators must be visible
Screen Readers
The breadcrumb structure announces:
- “Breadcrumb, navigation”
- Each link in order
- “Current page: [Page Name]” for last item
7. Testing Checklist
Unit Tests
Integration Tests
Accessibility Tests
Visual Tests
8. Common Mistakes & Anti-Patterns
❌ Importing breadcrumb primitives in pages
// ❌ WRONG: pages should never import @/shared/ui/breadcrumb
import { Breadcrumb, BreadcrumbList } from '@/shared/ui/breadcrumb';
// ✅ CORRECT: rely on the auto-generated header trail; for dynamic labels
// use useEntityBreadcrumb / useBreadcrumbLabel
import { useEntityBreadcrumb } from '@/shared/lib/hooks/useEntityBreadcrumb';
useEntityBreadcrumb(employee, (e) => e.profile?.full_name);
❌ Detail page without a dynamic label
// ❌ WRONG: trailing crumb is the static "Employee Details" instead of the
// employee's name. On mobile (which truncates to last 2 segments)
// users lose entity identity entirely.
export default function EmployeeDetailPage() {
const { data: employee } = useEmployeeDetail(id);
return <PageContainer><EmployeeProfile employee={employee} /></PageContainer>;
}
// ✅ CORRECT: trailing crumb is the employee name
export default function EmployeeDetailPage() {
const { data: employee } = useEmployeeDetail(id);
useEntityBreadcrumb(employee, (e) => e.profile?.full_name ?? 'Employee');
return <PageContainer><EmployeeProfile employee={employee} /></PageContainer>;
}
Run npm run audit:breadcrumb-coverage to see which detail pages still
need this migration.
❌ Duplicate “Back to X” link alongside breadcrumbs
// ❌ WRONG: page already shows breadcrumbs and a top-left back-icon button
<PageContainer breadcrumbs={[...]} showBreadcrumbs>
<Link to="/cl/in-basket"><ArrowLeft /> Back to In-Basket</Link>
...
</PageContainer>
// ✅ CORRECT: use DetailPageLayout backHref for the icon button; let the
// breadcrumb provide the parent link
<DetailPageLayout backHref="/cl/in-basket" title={item.title}>
...
</DetailPageLayout>
❌ Forgetting to add route labels
// ❌ WRONG: missing from route-labels.ts → audit:routes-navigation fails
// ✅ CORRECT: add a Title Case label to BASE_ROUTE_LABELS
'/my-feature/:id': 'Feature Details',
// ❌ WRONG
export const BASE_ROUTE_LABELS = {
'/hr/employees': 'employees', // lowercase
'/hr/employees/:id': 'Employee', // ambiguous (singular)
};
// ✅ CORRECT
export const BASE_ROUTE_LABELS = {
'/hr/employees': 'Employees',
'/hr/employees/:id': 'Employee Details',
};
❌ Breadcrumbs on module dashboards
// ❌ WRONG: breadcrumbs render on /hr/dashboard ("HR > Dashboard") — noise
// ✅ CORRECT: shouldShowBreadcrumbs hides them automatically on top-level
// module pages; do not override the policy on dashboards.
❌ Page H1 doesn’t match breadcrumb tail
// ❌ WRONG: H1 is generic, breadcrumb tail is the entity identifier
<h1>Statement Run</h1>
// breadcrumb: PM > Statement Runs > Mar 18, 2026 10:42 AM
// Mobile users (after truncation) only see the date — no context
// ✅ CORRECT: H1 includes the same identifier as the breadcrumb tail
<h1>Statement Run — {formatDate(run.run_started_at)}</h1>
9. Quick Reference
When to Show Breadcrumbs
| Page Type | Show Breadcrumbs? |
|---|
| Module dashboard | ❌ No |
| Top-level list | ❌ No |
| Detail/edit page | ✅ Yes |
| Nested settings | ✅ Yes |
| Multi-step workflow | ✅ Yes |
File Locations
| Purpose | File |
|---|
| Base components (do not import in pages) | src/shared/ui/breadcrumb.tsx |
| Desktop auto-gen | src/platform/navigation/Breadcrumbs.tsx |
| Mobile component | src/platform/navigation/MobileBreadcrumbs.tsx |
| Route labels (single source of truth) | src/platform/navigation/route-labels.ts |
Hub-tab labels (?tab=) | src/platform/navigation/hub-tab-labels.ts |
| Visibility policy / 2-segment allowlist | src/platform/navigation/utils/breadcrumb-visibility.ts |
PageContainer (with optional breadcrumbs prop) | src/shared/components/PageContainer.tsx |
DetailPageLayout (with backHref) | src/shared/components/DetailPageLayout.tsx |
| Dynamic-label hook | src/shared/lib/hooks/useEntityBreadcrumb.ts |
| Lower-level dynamic-label hook | src/platform/navigation/useBreadcrumbLabel.ts |
| Breadcrumb context | src/platform/navigation/BreadcrumbContext.tsx |
Audits
| Command | Checks |
|---|
npm run audit:routes-navigation | Every <Route> has a label; every nav entry resolves to a route; nav entries have labels |
npm run audit:breadcrumb-coverage | Detail/edit pages set a dynamic label via useEntityBreadcrumb / useBreadcrumbLabel / explicit breadcrumbs prop |
Standards
Navigation Guides
External References