Breadcrumbs seem simple but building them properly in Angular — automatically updated from the router, with correct aria attributes and structured data — takes a bit of setup. Here are the patterns I use.
Basic Static Breadcrumb Component
// breadcrumb.component.ts import { Component, Input } from '@angular/core' import { NgFor, NgIf } from '@angular/common' import { RouterLink } from '@angular/router' export interface BreadcrumbItem { label: string path?: string // undefined = current page (no link) icon?: string } @Component({ selector: 'app-breadcrumb', standalone: true, imports: [NgFor, NgIf, RouterLink], template: ` <nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0" [class.breadcrumb-flush]="flush"> <li *ngFor="let item of items; let last = last" class="breadcrumb-item" [class.active]="last" [attr.aria-current]="last ? 'page' : null" > <!-- Current page — no link --> <span *ngIf="last"> <span *ngIf="item.icon" class="me-1">{{ item.icon }}</span> {{ item.label }} </span> <!-- Ancestor — linked --> <a *ngIf="!last" [routerLink]="item.path" class="text-decoration-none"> <span *ngIf="item.icon" class="me-1">{{ item.icon }}</span> {{ item.label }} </a> </li> </ol> </nav> `, styles: [` .breadcrumb-flush { --bs-breadcrumb-divider: '›'; } .breadcrumb-item a { color: #fd4766; } .breadcrumb-item a:hover { color: #c0302e; } `] }) export class BreadcrumbComponent { @Input() items: BreadcrumbItem[] = [] @Input() flush = false }
Usage:
@Component({ selector: 'app-template-detail', standalone: true, imports: [BreadcrumbComponent], template: ` <div class="container py-4"> <app-breadcrumb [items]="crumbs" /> <!-- page content --> </div> ` }) export class TemplateDetailComponent { crumbs: BreadcrumbItem[] = [ { label: 'Home', path: '/', icon: '🏠' }, { label: 'Templates', path: '/templates' }, { label: 'Marvel Dashboard' }, // no path = current page ] }
Dynamic Router Breadcrumb Service
Automatically builds breadcrumbs from route data as the user navigates:
// breadcrumb.service.ts import { Injectable, signal } from '@angular/core' import { Router, NavigationEnd, ActivatedRoute } from '@angular/router' import { filter } from 'rxjs/operators' export interface BreadcrumbItem { label: string path: string icon?: string } @Injectable({ providedIn: 'root' }) export class BreadcrumbService { breadcrumbs = signal<BreadcrumbItem[]>([]) constructor(private router: Router, private route: ActivatedRoute) { this.router.events .pipe(filter((e) => e instanceof NavigationEnd)) .subscribe(() => { this.breadcrumbs.set(this.buildBreadcrumbs(this.route.root)) }) } private buildBreadcrumbs( route: ActivatedRoute, url = '', crumbs: BreadcrumbItem[] = [{ label: 'Home', path: '/', icon: '🏠' }] ): BreadcrumbItem[] { const children = route.children for (const child of children) { const segments = child.snapshot.url.map((s) => s.path) if (!segments.length) continue const path = url + '/' + segments.join('/') const label = child.snapshot.data['breadcrumb'] if (label) { crumbs.push({ label, path }) } this.buildBreadcrumbs(child, path, crumbs) } return crumbs } }
Route configuration with breadcrumb data:
// app.routes.ts export const routes: Routes = [ { path: 'templates', loadComponent: () => import('./templates/templates.component').then(m => m.TemplatesComponent), data: { breadcrumb: 'Templates' }, children: [ { path: ':id', loadComponent: () => import('./templates/template-detail.component').then(m => m.TemplateDetailComponent), data: { breadcrumb: 'Detail' }, // can also be set dynamically — see below } ] } ]
Breadcrumb component using the service:
// auto-breadcrumb.component.ts import { Component, inject } from '@angular/core' import { NgFor, NgIf } from '@angular/common' import { RouterLink } from '@angular/router' import { BreadcrumbService } from './breadcrumb.service' @Component({ selector: 'app-auto-breadcrumb', standalone: true, imports: [NgFor, NgIf, RouterLink], template: ` <nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0"> <li *ngFor="let crumb of svc.breadcrumbs(); let last = last" class="breadcrumb-item" [class.active]="last" [attr.aria-current]="last ? 'page' : null" > <span *ngIf="last"> <span *ngIf="crumb.icon">{{ crumb.icon }}</span> {{ crumb.label }} </span> <a *ngIf="!last" [routerLink]="crumb.path" class="text-decoration-none" style="color:#fd4766;"> <span *ngIf="crumb.icon">{{ crumb.icon }}</span> {{ crumb.label }} </a> </li> </ol> </nav> ` }) export class AutoBreadcrumbComponent { svc = inject(BreadcrumbService) }
Dynamic Label from Route Params
When the breadcrumb label comes from API data (e.g. the template name, not just "Detail"):
// template-detail.component.ts import { Component, inject, signal, OnInit } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { BreadcrumbService } from './breadcrumb.service' @Component({ selector: 'app-template-detail', standalone: true, imports: [AutoBreadcrumbComponent], template: ` <div class="container py-4"> <app-auto-breadcrumb /> <h1 class="fw-bold mt-3">{{ template()?.name }}</h1> </div> ` }) export class TemplateDetailComponent implements OnInit { private route = inject(ActivatedRoute) private breadcrumb = inject(BreadcrumbService) template = signal<{ name: string } | null>(null) ngOnInit() { const id = this.route.snapshot.params['id'] // Simulate API call Promise.resolve({ name: 'Marvel Dashboard' }).then((t) => { this.template.set(t) // Update the last breadcrumb label with the real name this.breadcrumb.breadcrumbs.update((crumbs) => { const updated = [...crumbs] updated[updated.length - 1] = { ...updated[updated.length - 1], label: t.name } return updated }) }) } }
Truncated Breadcrumb (Long Paths)
For deep navigation trees, collapse middle items:
// truncated-breadcrumb.component.ts import { Component, Input, signal, computed } from '@angular/core' import { NgFor, NgIf } from '@angular/common' import { RouterLink } from '@angular/router' import { BreadcrumbItem } from './breadcrumb.component' @Component({ selector: 'app-truncated-breadcrumb', standalone: true, imports: [NgFor, NgIf, RouterLink], template: ` <nav aria-label="breadcrumb"> <ol class="breadcrumb mb-0 flex-nowrap align-items-center"> <!-- First item always visible --> <li class="breadcrumb-item"> <a [routerLink]="items[0].path" style="color:#fd4766;">{{ items[0].label }}</a> </li> <!-- Collapsed middle items --> <ng-container *ngIf="items.length > maxVisible + 1"> <li class="breadcrumb-item"> <button class="btn btn-sm btn-link p-0 text-muted" (click)="expanded.set(true)" *ngIf="!expanded()" title="Show full path" >•••</button> </li> <ng-container *ngIf="expanded()"> <li *ngFor="let item of middleItems" class="breadcrumb-item"> <a [routerLink]="item.path" style="color:#fd4766;">{{ item.label }}</a> </li> </ng-container> </ng-container> <!-- Last items always visible --> <ng-container *ngIf="items.length <= maxVisible + 1"> <li *ngFor="let item of items.slice(1, -1)" class="breadcrumb-item"> <a [routerLink]="item.path" style="color:#fd4766;">{{ item.label }}</a> </li> </ng-container> <!-- Current page always visible --> <li class="breadcrumb-item active text-truncate" style="max-width:200px;" [attr.aria-current]="'page'"> {{ items[items.length - 1].label }} </li> </ol> </nav> ` }) export class TruncatedBreadcrumbComponent { @Input() items: BreadcrumbItem[] = [] @Input() maxVisible = 2 expanded = signal(false) get middleItems(): BreadcrumbItem[] { return this.items.slice(1, -1) } }
Key Classes Reference
| Class / API | Purpose |
|---|---|
breadcrumb | Bootstrap breadcrumb container |
breadcrumb-item | Individual crumb |
breadcrumb-item active | Styles current page item |
aria-current="page" | Accessibility — marks current page for screen readers |
--bs-breadcrumb-divider | CSS variable for custom separator |
signal<BreadcrumbItem[]>([]) | Reactive breadcrumbs array |
Router events + NavigationEnd | Rebuild crumbs on each navigation |
route.snapshot.data['breadcrumb'] | Read breadcrumb label from route data |
Frequently Asked Questions
Related
Need a Full Angular + Bootstrap Admin Dashboard?
Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.
Browse Templates →Use code FIRST30 for 30% off.