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 / APIPurpose
breadcrumbBootstrap breadcrumb container
breadcrumb-itemIndividual crumb
breadcrumb-item activeStyles current page item
aria-current="page"Accessibility — marks current page for screen readers
--bs-breadcrumb-dividerCSS variable for custom separator
signal<BreadcrumbItem[]>([])Reactive breadcrumbs array
Router events + NavigationEndRebuild crumbs on each navigation
route.snapshot.data['breadcrumb']Read breadcrumb label from route data

Frequently Asked Questions

Subscribe to Router events in a breadcrumb service, walk the ActivatedRoute tree collecting route.data['breadcrumb'] values from each segment, and expose the result as a signal. Inject the service in a BreadcrumbComponent and render the array with @for.
Add a data property to each route definition: { path: 'templates', component: TemplatesComponent, data: { breadcrumb: 'Templates' } }. Your breadcrumb service reads route.data['breadcrumb'] as it walks the active route tree.
Override the --bs-breadcrumb-divider CSS variable in your global styles or the component's styles array: :root { --bs-breadcrumb-divider: '›'; }. Or use a CSS content value with Unicode: --bs-breadcrumb-divider: url("data:image/svg+xml,...") for an SVG arrow.

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.