How to use mat-tab-group in Angular (Material) — practical guide, tips & how to modify tabs?

Leader posted 4 min read

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.

More Posts

Beyond the Defaults: A Guide to Customizing Angular Material Themes

Sunny - Sep 4

Debugging Angular Without Knowing the Codebase?

Sunny - Sep 10

You Can Be an Angular Contributor: A Guide to Your First Open-Source PR

Sunny - Aug 5

From Idea to Angular App: Prototyping in Google AI Studio

Sunny - Nov 5

How to Install and Use ComfyUI and SwarmUI on Massed Compute and RunPod Private Cloud GPU Services

FurkanGozukara - Oct 18
chevron_left