Bootstrap 5 tabs work great in Angular โ€” the markup is simple and the active state integrates naturally with signals. Here are the patterns from basic to router-linked to fully dynamic.

Basic Reusable Tab Component

// tabs.component.ts
import { Component, Input, signal, computed, ContentChildren, QueryList, AfterContentInit } from '@angular/core'
import { NgFor, NgIf, NgClass } from '@angular/common'

export interface Tab {
  id: string
  label: string
  badge?: string | number
  icon?: string
  disabled?: boolean
}

@Component({
  selector: 'app-tabs',
  standalone: true,
  imports: [NgFor, NgIf, NgClass],
  template: `
    <!-- Tab Headers -->
    <ul class="nav" [ngClass]="'nav-' + variant">
      <li class="nav-item" *ngFor="let tab of tabs">
        <button
          class="nav-link d-flex align-items-center gap-2"
          [class.active]="activeId() === tab.id"
          [class.disabled]="tab.disabled"
          [disabled]="tab.disabled"
          (click)="setActive(tab.id)"
          type="button"
          [attr.aria-selected]="activeId() === tab.id"
        >
          <span *ngIf="tab.icon">{{ tab.icon }}</span>
          {{ tab.label }}
          <span
            *ngIf="tab.badge !== undefined"
            class="badge rounded-pill"
            [class.bg-danger]="activeId() === tab.id"
            [class.text-bg-secondary]="activeId() !== tab.id"
          >{{ tab.badge }}</span>
        </button>
      </li>
    </ul>

    <!-- Tab Content -->
    <div class="tab-content mt-3">
      <ng-content></ng-content>
    </div>
  `,
  styles: [`
    .nav-tabs .nav-link.active {
      color: #fd4766;
      border-color: #dee2e6 #dee2e6 #fff;
      font-weight: 600;
    }
    .nav-tabs .nav-link:not(.active) {
      color: #6c757d;
    }
    .nav-tabs .nav-link:hover:not(.active):not(.disabled) {
      color: #fd4766;
    }
    .nav-pills .nav-link.active {
      background: #fd4766;
    }
  `]
})
export class TabsComponent {
  @Input() tabs: Tab[] = []
  @Input() variant: 'tabs' | 'pills' | 'underline' = 'tabs'
  @Input() defaultTab = ''

  activeId = signal('')

  ngOnInit() {
    this.activeId.set(this.defaultTab || (this.tabs[0]?.id ?? ''))
  }

  setActive(id: string) {
    this.activeId.set(id)
  }

  isActive(id: string): boolean {
    return this.activeId() === id
  }
}

Tab Panel component for content:

// tab-panel.component.ts
import { Component, Input } from '@angular/core'
import { NgIf } from '@angular/common'

@Component({
  selector: 'app-tab-panel',
  standalone: true,
  imports: [NgIf],
  template: `
    <div
      class="tab-pane fade"
      [class.show]="active"
      [class.active]="active"
    >
      <ng-content *ngIf="active"></ng-content>
    </div>
  `
})
export class TabPanelComponent {
  @Input() active = false
}

Usage:

@Component({
  selector: 'app-settings',
  standalone: true,
  imports: [TabsComponent, TabPanelComponent],
  template: `
    <div class="container py-4">
      <app-tabs [tabs]="tabs" variant="tabs">
        <app-tab-panel [active]="tabs$.isActive('profile')">
          <h5>Profile Settings</h5>
          <p class="text-muted">Update your name and email.</p>
        </app-tab-panel>
        <app-tab-panel [active]="tabs$.isActive('billing')">
          <h5>Billing</h5>
          <p class="text-muted">Manage your subscription.</p>
        </app-tab-panel>
        <app-tab-panel [active]="tabs$.isActive('security')">
          <h5>Security</h5>
          <p class="text-muted">Password and 2FA settings.</p>
        </app-tab-panel>
      </app-tabs>
    </div>
  `
})
export class SettingsComponent {
  tabs: Tab[] = [
    { id: 'profile',  label: 'Profile',  icon: '๐Ÿ‘ค' },
    { id: 'billing',  label: 'Billing',  icon: '๐Ÿ’ณ', badge: 1 },
    { id: 'security', label: 'Security', icon: '๐Ÿ”’' },
  ]
}

Router-Linked Tabs

Link each tab to a child route โ€” URL updates on tab change and refresh restores the active tab:

// account.component.ts
import { Component } from '@angular/core'
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'
import { NgFor } from '@angular/common'

@Component({
  selector: 'app-account',
  standalone: true,
  imports: [RouterLink, RouterLinkActive, RouterOutlet, NgFor],
  template: `
    <div class="container py-4">
      <h4 class="fw-bold mb-4">Account Settings</h4>

      <!-- Router tabs -->
      <ul class="nav nav-tabs mb-4">
        <li class="nav-item" *ngFor="let tab of tabs">
          <a
            class="nav-link d-flex align-items-center gap-2"
            [routerLink]="tab.path"
            routerLinkActive="active"
            [routerLinkActiveOptions]="{ exact: false }"
          >
            {{ tab.icon }} {{ tab.label }}
          </a>
        </li>
      </ul>

      <!-- Active child route renders here -->
      <router-outlet />
    </div>
  `,
  styles: [`
    .nav-tabs .nav-link.active { color: #fd4766; font-weight: 600; }
    .nav-tabs .nav-link:hover:not(.active) { color: #fd4766; }
  `]
})
export class AccountComponent {
  tabs = [
    { path: 'profile',       label: 'Profile',       icon: '๐Ÿ‘ค' },
    { path: 'notifications', label: 'Notifications', icon: '๐Ÿ””' },
    { path: 'billing',       label: 'Billing',       icon: '๐Ÿ’ณ' },
    { path: 'security',      label: 'Security',      icon: '๐Ÿ”’' },
  ]
}

Routes:

// account.routes.ts
export const accountRoutes: Routes = [
  { path: '', redirectTo: 'profile', pathMatch: 'full' },
  { path: 'profile',       loadComponent: () => import('./profile.component').then(m => m.ProfileComponent) },
  { path: 'notifications', loadComponent: () => import('./notifications.component').then(m => m.NotificationsComponent) },
  { path: 'billing',       loadComponent: () => import('./billing.component').then(m => m.BillingComponent) },
  { path: 'security',      loadComponent: () => import('./security.component').then(m => m.SecurityComponent) },
]

Dynamic Tabs with signal()

// dynamic-tabs.component.ts
import { Component, signal, computed } from '@angular/core'
import { NgFor, NgIf } from '@angular/common'

interface DynamicTab {
  id: string
  label: string
  content: string
  closable?: boolean
}

@Component({
  selector: 'app-dynamic-tabs',
  standalone: true,
  imports: [NgFor, NgIf],
  template: `
    <div>
      <!-- Tab headers -->
      <ul class="nav nav-tabs">
        <li class="nav-item" *ngFor="let tab of tabs()">
          <button
            class="nav-link d-flex align-items-center gap-2"
            [class.active]="activeId() === tab.id"
            (click)="activeId.set(tab.id)"
            type="button"
          >
            {{ tab.label }}
            <span
              *ngIf="tab.closable"
              class="ms-1 opacity-50"
              style="font-size:0.65rem;cursor:pointer;"
              (click)="$event.stopPropagation(); close(tab.id)"
            >โœ•</span>
          </button>
        </li>
        <!-- Add tab button -->
        <li class="nav-item">
          <button
            class="nav-link text-muted"
            (click)="addTab()"
            type="button"
            title="Add tab"
          >+ Add</button>
        </li>
      </ul>

      <!-- Tab content -->
      <div class="border border-top-0 rounded-bottom p-4">
        <ng-container *ngFor="let tab of tabs()">
          <div *ngIf="activeId() === tab.id">
            <p class="mb-0">{{ tab.content }}</p>
          </div>
        </ng-container>
      </div>
    </div>
  `,
  styles: [`
    .nav-tabs .nav-link.active { color: #fd4766; font-weight: 600; }
  `]
})
export class DynamicTabsComponent {
  tabs = signal<DynamicTab[]>([
    { id: 'tab-1', label: 'Overview',  content: 'Dashboard overview content.',   closable: false },
    { id: 'tab-2', label: 'Analytics', content: 'Analytics content here.',       closable: true  },
    { id: 'tab-3', label: 'Reports',   content: 'Reports and exports here.',     closable: true  },
  ])

  activeId = signal('tab-1')
  private counter = 3

  addTab() {
    this.counter++
    const newTab: DynamicTab = {
      id: `tab-${this.counter}`,
      label: `Tab ${this.counter}`,
      content: `Content for tab ${this.counter}.`,
      closable: true,
    }
    this.tabs.update((t) => [...t, newTab])
    this.activeId.set(newTab.id)
  }

  close(id: string) {
    const remaining = this.tabs().filter((t) => t.id !== id)
    this.tabs.set(remaining)
    if (this.activeId() === id) {
      this.activeId.set(remaining[remaining.length - 1]?.id ?? '')
    }
  }
}

Vertical Tabs

// vertical-tabs.component.ts
import { Component, signal } from '@angular/core'
import { NgFor, NgIf } from '@angular/common'

@Component({
  selector: 'app-vertical-tabs',
  standalone: true,
  imports: [NgFor, NgIf],
  template: `
    <div class="d-flex gap-4">
      <!-- Vertical nav -->
      <div class="nav flex-column nav-pills" style="min-width:180px;">
        <button
          *ngFor="let tab of tabs"
          class="nav-link text-start mb-1 d-flex align-items-center gap-2"
          [class.active]="activeId() === tab.id"
          [style.background]="activeId() === tab.id ? '#fd4766' : ''"
          (click)="activeId.set(tab.id)"
          type="button"
        >
          {{ tab.icon }} {{ tab.label }}
        </button>
      </div>

      <!-- Content area -->
      <div class="flex-grow-1 border rounded-3 p-4">
        <ng-container *ngFor="let tab of tabs">
          <div *ngIf="activeId() === tab.id">
            <h5 class="fw-bold mb-2">{{ tab.label }}</h5>
            <p class="text-muted">{{ tab.description }}</p>
          </div>
        </ng-container>
      </div>
    </div>
  `
})
export class VerticalTabsComponent {
  activeId = signal('profile')

  tabs = [
    { id: 'profile',  icon: '๐Ÿ‘ค', label: 'Profile',        description: 'Update your profile information and avatar.' },
    { id: 'billing',  icon: '๐Ÿ’ณ', label: 'Billing',         description: 'Manage your subscription and payment methods.' },
    { id: 'security', icon: '๐Ÿ”’', label: 'Security',        description: 'Password, two-factor auth and active sessions.' },
    { id: 'api',      icon: '๐Ÿ”‘', label: 'API Keys',        description: 'Generate and revoke API keys for integrations.' },
    { id: 'team',     icon: '๐Ÿ‘ฅ', label: 'Team',            description: 'Invite members and manage roles.' },
  ]
}

Key Classes Reference

Class / APIPurpose
nav nav-tabsHorizontal tab bar
nav nav-pillsPill-style tab bar
nav flex-column nav-pillsVertical tab bar
nav-link activeActive tab styling
routerLinkActive="active"Auto-applies active class on matched route
tab-pane fade show activeBootstrap's transition for tab content panels
signal()Reactive active tab id
ViewContainerRef.createComponent()Fully dynamic component tab content

Frequently Asked Questions

Use Bootstrap's nav-tabs class with a nav element, and bind the active class to the selected tab using a signal or a simple boolean comparison. Show the corresponding content panel using @if or [hidden] based on the active tab value.
Link each tab to a child route using routerLink. Use routerLinkActive='active' to apply Bootstrap's active class automatically. Add <router-outlet> in the tab content area for the child route to render into.
Define a tabs array with an id, label and optionally a component type. Use @for to render the tab headers. For content, either use @switch on the active tab id, or dynamically create components using ViewContainerRef.createComponent() for fully dynamic rendering.

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.