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 / API | Purpose |
|---|---|
nav nav-tabs | Horizontal tab bar |
nav nav-pills | Pill-style tab bar |
nav flex-column nav-pills | Vertical tab bar |
nav-link active | Active tab styling |
routerLinkActive="active" | Auto-applies active class on matched route |
tab-pane fade show active | Bootstrap'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.