mat-tab-group from Angular Material is a polished, accessible tab component that makes organizing UI into multiple panels easy. This post covers how to use it, best practices, and all the common ways you might modify tabs (dynamic tabs, programmatic selection, lazy content, styling, accessibility, and performance tips).
✅ Why use mat-tab-group?
- Built-in accessibility and keyboard navigation.
- Clean UX for grouped content (settings, dashboards, profile pages, etc.).
- Works well with Angular change detection and templates.
- Easy to style to match your app.
1) Quick setup
Install Angular Material (if not already):
ng add @angular/material
Import the tabs module in your Angular module:
// app.module.ts
import { MatTabsModule } from '@angular/material/tabs';
@NgModule({
imports: [
// ...
MatTabsModule,
],
})
export class AppModule {}
2) Basic usage
<mat-tab-group>
<mat-tab label="First">Content for first tab</mat-tab>
<mat-tab label="Second">Content for second tab</mat-tab>
<mat-tab label="Third">Content for third tab</mat-tab>
</mat-tab-group>
3) Best practice: lazy-loading tab content
Use ng-template matTabContent to avoid rendering heavy DOM for inactive tabs:
<mat-tab-group>
<mat-tab label="Light">
<ng-template matTabContent>
<!-- heavy/expensive content goes here -->
<app-chart *ngIf="showChart"></app-chart>
</ng-template>
</mat-tab>
<mat-tab label="Info">
<ng-template matTabContent>
<p>Some info...</p>
</ng-template>
</mat-tab>
</mat-tab-group>
Why? Lazy loading reduces initial render cost and speeds up tab group mounting.
4) Dynamic tabs — add/remove tabs at runtime
A very common need is to generate tabs from an array and let users open/close tabs.
Component TypeScript
// dynamic-tabs.component.ts
import { Component, ViewChild } from '@angular/core';
import { MatTabGroup } from '@angular/material/tabs';
interface Tab {
id: string;
title: string;
content: string;
closable?: boolean;
}
@Component({
selector: 'app-dynamic-tabs',
templateUrl: './dynamic-tabs.component.html',
})
export class DynamicTabsComponent {
@ViewChild(MatTabGroup) tabGroup!: MatTabGroup;
tabs: Tab[] = [
{ id: 'home', title: 'Home', content: 'Welcome home' },
{ id: 'about', title: 'About', content: 'About us' },
];
selectedIndex = 0;
addTab() {
const newTab: Tab = {
id: `tab-${Date.now()}`,
title: 'New tab',
content: 'Dynamically created content',
closable: true,
};
this.tabs.push(newTab);
// select the newly added tab on next tick
setTimeout(() => (this.selectedIndex = this.tabs.length - 1));
}
closeTab(index: number) {
this.tabs.splice(index, 1);
// adjust selected index safely
if (this.selectedIndex >= this.tabs.length) {
this.selectedIndex = Math.max(0, this.tabs.length - 1);
}
}
}
Template
<!-- dynamic-tabs.component.html -->
<button mat-raised-button (click)="addTab()">Add tab</button>
<mat-tab-group [(selectedIndex)]="selectedIndex">
<mat-tab *ngFor="let t of tabs; let i = index; trackBy: trackById">
<ng-template mat-tab-label>
{{ t.title }}
<button *ngIf="t.closable" mat-icon-button (click)="closeTab(i); $event.stopPropagation();" aria-label="Close tab">
<mat-icon>close</mat-icon>
</button>
</ng-template>
<ng-template matTabContent>
<div class="tab-content">{{ t.content }}</div>
</ng-template>
</mat-tab>
</mat-tab-group>
trackBy function (optional, for performance)
Add to the component:
trackById(index: number, tab: Tab) {
return tab.id;
}
Notes:
- Use
[(selectedIndex)] to keep selection synchronized with your model.
- Stop propagation on close button to avoid changing selection when closing.
setTimeout for selection is a simple sync trick; you can also run change detection manually if needed.
5) Programmatic control & events
selectedIndex property: set which tab is active.
(selectedTabChange) emits MatTabChangeEvent when user selects a tab.
<mat-tab-group (selectedTabChange)="onTabChange($event)" [(selectedIndex)]="selectedIndex">
...
</mat-tab-group>
onTabChange(event: MatTabChangeEvent) {
console.log('Selected tab index', event.index, 'label', event.tab.textLabel);
}
You can get a reference to the MatTabGroup with @ViewChild to call methods or inspect tabs.
6) Styling & theming
Prefer using your global stylesheet or component-level styles with :host ::ng-deep only when necessary.
Example (in styles.scss or global stylesheet):
/* style the ink bar and labels globally */
.mat-tab-group .mat-ink-bar {
height: 3px;
}
.mat-tab-label {
font-weight: 600;
text-transform: none;
}
If modifying from inside a component and view encapsulation blocks styling, either:
- Put styles in
styles.scss (global), or
- Use
::ng-deep carefully (deprecated but still used), or
- Set
encapsulation: ViewEncapsulation.None for that component.
7) Accessibility & keyboard navigation
Angular Material tabs provide keyboard support and ARIA attributes out of the box. Still:
- Ensure meaningful tab labels.
- Avoid long interactive content without landmarks inside tabs — screen reader users benefit from headings.
- If you add custom close buttons or controls, provide
aria-labels.
8) Routing + tabs (optional patterns)
If you want tab-per-route behavior, use:
mat-tab-nav-bar + routerLink for navigation-like tabs, or
- Synchronize
selectedIndex with the router (listen to ActivatedRoute and set the selected tab).
Use mat-tab-nav-bar when each tab should be its own URL.
9) Performance tips
- Use
ng-template matTabContent to lazy-render heavy components.
- Use
trackBy in *ngFor for dynamic tabs to avoid unnecessary DOM churn.
- If you render complex child components, consider detaching change detection during creation and reattaching after initialization (advanced).
10) Is it possible to modify tabs? (Short answer: Yes — many ways)
You can:
- Dynamically add/remove tabs by changing the backing array (Angular updates the DOM).
- Change labels and content by updating the model bound to the tab.
- Programmatically select a tab using
selectedIndex or @ViewChild(MatTabGroup).
- Do multi-file or dynamic behavior like loading content from a server and injecting components into a tab.
- Style and theme tabs by overriding Material styles globally or at component-level.
All modifications are supported and are standard Angular patterns (data-driven UI, event bindings, ViewChild).
Example: Putting it all together (short)
<!-- app.component.html -->
<button (click)="addTab()">+ New</button>
<mat-tab-group [(selectedIndex)]="selectedIndex" (selectedTabChange)="onTabChange($event)">
<mat-tab *ngFor="let t of tabs; let i = index; trackBy: trackById">
<ng-template mat-tab-label>
{{ t.title }}
<button *ngIf="t.closable" mat-icon-button (click)="closeTab(i); $event.stopPropagation()">
<mat-icon>close</mat-icon>
</button>
</ng-template>
<ng-template matTabContent>
<app-heavy-widget *ngIf="t.lazyLoaded" [data]="t.data"></app-heavy-widget>
<div *ngIf="!t.lazyLoaded">{{ t.content }}</div>
</ng-template>
</mat-tab>
</mat-tab-group>
Final checklist / best practices
- Use
matTabContent for heavy content (lazy loading).
- Drive tabs from data (array) and use
trackBy.
- Keep tab labels short and descriptive.
- Use
selectedIndex for programmatic control.
- Prefer global or scoped styles rather than
::ng-deep where possible.
- Maintain accessibility by adding
aria labels to controls.
- Always review generated DOM when dynamically adding components to tabs.