Docs
/
Angular
Chapter 14
14 — Angular Material & CDK
What is Angular Material?
Angular Material is a UI component library implementing Google's Material Design for Angular. The CDK (Component Dev Kit) provides behavior primitives without styling.
ng add @angular/material
# Selects a theme, typography, animations
Setup
// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimationsAsync(), // Required for Material animations
provideRouter(routes),
],
};
Import Components Individually (Standalone)
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTableModule } from '@angular/material/table';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
@Component({
imports: [
MatButtonModule,
MatInputModule,
MatFormFieldModule,
],
})
Common Components
Buttons
<button mat-button>Basic</button>
<button mat-raised-button color="primary">Raised</button>
<button mat-flat-button color="accent">Flat</button>
<button mat-stroked-button>Outlined</button>
<button mat-icon-button><mat-icon>edit</mat-icon></button>
<button mat-fab><mat-icon>add</mat-icon></button>
<button mat-mini-fab><mat-icon>add</mat-icon></button>
Form Fields & Inputs
<mat-form-field appearance="outline">
<mat-label>Email</mat-label>
<input matInput formControlName="email" type="email">
<mat-icon matSuffix>email</mat-icon>
<mat-hint>We'll never share your email</mat-hint>
<mat-error>Valid email required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Role</mat-label>
<mat-select formControlName="role">
@for (role of roles; track role) {
<mat-option [value]="role">{{ role | titlecase }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Bio</mat-label>
<textarea matInput formControlName="bio" rows="4"></textarea>
<mat-hint align="end">{{ bio.value.length }} / 500</mat-hint>
</mat-form-field>
Table
@Component({
imports: [MatTableModule, MatSortModule, MatPaginatorModule],
template: `
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
<td mat-cell *matCellDef="let user">{{ user.name }}</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
<td mat-cell *matCellDef="let user">{{ user.email }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let user">
<button mat-icon-button (click)="edit(user)"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="delete(user)"><mat-icon>delete</mat-icon></button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50]" showFirstLastButtons />
`,
})
export class UserTableComponent implements AfterViewInit {
displayedColumns = ['name', 'email', 'actions'];
dataSource = new MatTableDataSource<User>([]);
@ViewChild(MatSort) sort!: MatSort;
@ViewChild(MatPaginator) paginator!: MatPaginator;
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
}
applyFilter(value: string) {
this.dataSource.filter = value.trim().toLowerCase();
}
}
Dialog
// dialog component
@Component({
imports: [MatDialogModule, MatButtonModule],
template: `
<h2 mat-dialog-title>Confirm Delete</h2>
<mat-dialog-content>
Are you sure you want to delete {{ data.name }}?
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-flat-button color="warn" [mat-dialog-close]="true">Delete</button>
</mat-dialog-actions>
`,
})
export class ConfirmDialogComponent {
data = inject<{ name: string }>(MAT_DIALOG_DATA);
}
// Opening the dialog
private dialog = inject(MatDialog);
confirmDelete(user: User) {
const ref = this.dialog.open(ConfirmDialogComponent, {
width: '400px',
data: { name: user.name },
});
ref.afterClosed().subscribe(confirmed => {
if (confirmed) this.deleteUser(user.id);
});
}
Snackbar (Toast)
private snackBar = inject(MatSnackBar);
showSuccess() {
this.snackBar.open('User saved successfully!', 'Close', {
duration: 3000,
horizontalPosition: 'end',
verticalPosition: 'top',
panelClass: ['snackbar-success'],
});
}
Theming
Custom Theme (styles.scss)
@use '@angular/material' as mat;
// Define color palettes
$primary: mat.m2-define-palette(mat.$m2-indigo-palette);
$accent: mat.m2-define-palette(mat.$m2-pink-palette, A200);
$warn: mat.m2-define-palette(mat.$m2-red-palette);
// Create theme
$theme: mat.m2-define-light-theme((
color: (primary: $primary, accent: $accent, warn: $warn),
typography: mat.m2-define-typography-config(),
density: 0,
));
// Apply theme
@include mat.all-component-themes($theme);
// Dark theme variant
.dark-theme {
$dark-theme: mat.m2-define-dark-theme((
color: (primary: $primary, accent: $accent, warn: $warn),
));
@include mat.all-component-colors($dark-theme);
}
Dark Mode Toggle
@Injectable({ providedIn: 'root' })
export class ThemeService {
isDark = signal(false);
toggle() {
this.isDark.update(v => !v);
document.body.classList.toggle('dark-theme', this.isDark());
}
}
CDK — Behavior Primitives
Drag & Drop
import { CdkDragDrop, moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
@Component({
imports: [DragDropModule],
template: `
<div cdkDropList (cdkDropListDropped)="drop($event)">
@for (item of items; track item.id) {
<div cdkDrag class="drag-item">{{ item.name }}</div>
}
</div>
`,
})
export class SortableListComponent {
items = [{ id: 1, name: 'Item A' }, { id: 2, name: 'Item B' }, { id: 3, name: 'Item C' }];
drop(event: CdkDragDrop<typeof this.items>) {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
}
Virtual Scrolling (CDK)
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="48" class="list-viewport">
<div *cdkVirtualFor="let item of items; trackBy: trackById" class="list-item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`.list-viewport { height: 400px; }`],
})
export class VirtualListComponent {
items = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
}
Overlay (CDK)
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
// Create floating overlay programmatically
private overlay = inject(Overlay);
openTooltip(origin: ElementRef) {
const positionStrategy = this.overlay.position()
.flexibleConnectedTo(origin)
.withPositions([{ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' }]);
const overlayRef = this.overlay.create({ positionStrategy, hasBackdrop: true });
const portal = new ComponentPortal(TooltipComponent);
overlayRef.attach(portal);
overlayRef.backdropClick().subscribe(() => overlayRef.dispose());
}
Key Takeaways
- Import Material components individually — don't import everything
- Use
MatTableDataSourcewithMatSortandMatPaginatorfor powerful tables MatDialogreturns an observable fromafterClosed()— perfect for confirmation flows- Use CDK for behavior without Material styling: drag-drop, virtual scroll, overlay
- Virtual scrolling (
cdk-virtual-scroll-viewport) is essential for large lists (1000+ items) - Create a custom theme with
m2-define-light-themeandm2-define-dark-theme - Prefer
provideAnimationsAsync()overprovideAnimations()for lazy-loaded animations