Skip to main content
Angular Material

Angular Material Overview

Estimated Time: 3 hours | Difficulty: Intermediate | Prerequisites: Components, Forms, Accessibility
Angular Material is a comprehensive UI component library that implements Google’s Material Design. The Component Dev Kit (CDK) provides behavior primitives for building custom components.
┌─────────────────────────────────────────────────────────────────────────┐
│                    Angular Material Architecture                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                    Your Application                              │   │
│   └───────────────────────────┬─────────────────────────────────────┘   │
│                               │                                          │
│   ┌───────────────────────────┴─────────────────────────────────────┐   │
│   │                    Angular Material                              │   │
│   │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐  │   │
│   │  │   Buttons   │ │   Forms     │ │   Tables    │ │  Dialogs  │  │   │
│   │  │   Cards     │ │   Inputs    │ │   Lists     │ │  Snackbar │  │   │
│   │  │   Chips     │ │   Selects   │ │   Trees     │ │  Tooltips │  │   │
│   │  └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘  │   │
│   └───────────────────────────┬─────────────────────────────────────┘   │
│                               │                                          │
│   ┌───────────────────────────┴─────────────────────────────────────┐   │
│   │                    Component Dev Kit (CDK)                       │   │
│   │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐  │   │
│   │  │  Overlay    │ │   Portal    │ │  Drag/Drop  │ │   A11y    │  │   │
│   │  │  Scrolling  │ │   Platform  │ │   Layout    │ │  Stepper  │  │   │
│   │  │  Clipboard  │ │  Observers  │ │   Table     │ │   Tree    │  │   │
│   │  └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘  │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Setup

# Add Angular Material
ng add @angular/material

# This will:
# 1. Install @angular/material, @angular/cdk, @angular/animations
# 2. Add theme to angular.json
# 3. Add global typography and fonts
# 4. Set up animations

Modern Configuration

// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimationsAsync(),
    // ... other providers
  ]
};

// styles.scss
@use '@angular/material' as mat;

// Include core styles
@include mat.core();

// Define custom theme
$primary: mat.m2-define-palette(mat.$m2-indigo-palette);
$accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
$warn: mat.m2-define-palette(mat.$m2-red-palette);

$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
.dark-theme {
  $dark-theme: mat.m2-define-dark-theme((
    color: (
      primary: $primary,
      accent: $accent,
      warn: $warn,
    ),
  ));
  @include mat.all-component-colors($dark-theme);
}

Core Components

Buttons & Indicators

// button-showcase.component.ts
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatBadgeModule } from '@angular/material/badge';

@Component({
  selector: 'app-button-showcase',
  standalone: true,
  imports: [
    MatButtonModule,
    MatIconModule,
    MatProgressSpinnerModule,
    MatBadgeModule
  ],
  template: `
    <!-- Basic 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 color="warn">Stroked</button>
    
    <!-- Icon buttons -->
    <button mat-icon-button aria-label="Settings">
      <mat-icon>settings</mat-icon>
    </button>
    
    <button mat-fab color="primary" aria-label="Add">
      <mat-icon>add</mat-icon>
    </button>
    
    <button mat-mini-fab color="accent" aria-label="Edit">
      <mat-icon>edit</mat-icon>
    </button>
    
    <!-- Extended FAB -->
    <button mat-fab extended color="primary">
      <mat-icon>add</mat-icon>
      Create New
    </button>
    
    <!-- Loading button -->
    <button mat-raised-button color="primary" [disabled]="loading()">
      @if (loading()) {
        <mat-spinner diameter="20"></mat-spinner>
      } @else {
        Submit
      }
    </button>
    
    <!-- Button with badge -->
    <button mat-icon-button matBadge="5" matBadgeColor="warn">
      <mat-icon>notifications</mat-icon>
    </button>
  `
})
export class ButtonShowcaseComponent {
  loading = signal(false);
}

Form Controls

// form-controls.component.ts
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatRadioModule } from '@angular/material/radio';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';

@Component({
  selector: 'app-form-controls',
  standalone: true,
  imports: [
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatSelectModule,
    MatAutocompleteModule,
    MatDatepickerModule,
    MatNativeDateModule,
    MatCheckboxModule,
    MatRadioModule,
    MatSlideToggleModule,
    MatSliderModule
  ],
  template: `
    <form [formGroup]="form" class="form-container">
      <!-- Text input with validation -->
      <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>
        @if (form.get('email')?.hasError('required')) {
          <mat-error>Email is required</mat-error>
        }
        @if (form.get('email')?.hasError('email')) {
          <mat-error>Invalid email format</mat-error>
        }
      </mat-form-field>
      
      <!-- Select -->
      <mat-form-field appearance="outline">
        <mat-label>Country</mat-label>
        <mat-select formControlName="country">
          @for (country of countries; track country.code) {
            <mat-option [value]="country.code">
              {{ country.name }}
            </mat-option>
          }
        </mat-select>
      </mat-form-field>
      
      <!-- Autocomplete -->
      <mat-form-field appearance="outline">
        <mat-label>City</mat-label>
        <input 
          matInput 
          formControlName="city"
          [matAutocomplete]="auto"
        />
        <mat-autocomplete #auto="matAutocomplete">
          @for (city of filteredCities(); track city) {
            <mat-option [value]="city">{{ city }}</mat-option>
          }
        </mat-autocomplete>
      </mat-form-field>
      
      <!-- Date picker -->
      <mat-form-field appearance="outline">
        <mat-label>Birth Date</mat-label>
        <input matInput [matDatepicker]="picker" formControlName="birthDate" />
        <mat-datepicker-toggle matSuffix [for]="picker" />
        <mat-datepicker #picker />
      </mat-form-field>
      
      <!-- Checkbox -->
      <mat-checkbox formControlName="terms">
        I agree to the terms and conditions
      </mat-checkbox>
      
      <!-- Radio buttons -->
      <mat-radio-group formControlName="gender">
        <mat-radio-button value="male">Male</mat-radio-button>
        <mat-radio-button value="female">Female</mat-radio-button>
        <mat-radio-button value="other">Other</mat-radio-button>
      </mat-radio-group>
      
      <!-- Slide toggle -->
      <mat-slide-toggle formControlName="notifications">
        Enable notifications
      </mat-slide-toggle>
      
      <!-- Slider -->
      <mat-slider min="0" max="100" step="1" showTickMarks discrete>
        <input matSliderThumb formControlName="volume" />
      </mat-slider>
    </form>
  `
})
export class FormControlsComponent {
  form = inject(FormBuilder).group({
    email: ['', [Validators.required, Validators.email]],
    country: [''],
    city: [''],
    birthDate: [null],
    terms: [false],
    gender: [''],
    notifications: [true],
    volume: [50]
  });
  
  countries = [
    { code: 'us', name: 'United States' },
    { code: 'uk', name: 'United Kingdom' },
    { code: 'de', name: 'Germany' }
  ];
  
  cities = ['New York', 'Los Angeles', 'Chicago', 'Houston'];
  
  filteredCities = computed(() => {
    const value = this.form.get('city')?.value?.toLowerCase() ?? '';
    return this.cities.filter(city => 
      city.toLowerCase().includes(value)
    );
  });
}

Data Table

// data-table.component.ts
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { SelectionModel } from '@angular/cdk/collections';

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive';
}

@Component({
  selector: 'app-data-table',
  standalone: true,
  imports: [
    MatTableModule,
    MatPaginatorModule,
    MatSortModule,
    MatCheckboxModule,
    MatChipsModule,
    MatIconModule,
    MatButtonModule,
    MatMenuModule
  ],
  template: `
    <div class="table-container">
      <!-- Search and filters -->
      <mat-form-field appearance="outline" class="search-field">
        <mat-label>Search</mat-label>
        <input matInput (keyup)="applyFilter($event)" #searchInput />
        <mat-icon matSuffix>search</mat-icon>
      </mat-form-field>
      
      <table mat-table [dataSource]="dataSource" matSort>
        <!-- Checkbox Column -->
        <ng-container matColumnDef="select">
          <th mat-header-cell *matHeaderCellDef>
            <mat-checkbox
              (change)="$event ? toggleAll() : null"
              [checked]="selection.hasValue() && isAllSelected()"
              [indeterminate]="selection.hasValue() && !isAllSelected()"
            />
          </th>
          <td mat-cell *matCellDef="let row">
            <mat-checkbox
              (click)="$event.stopPropagation()"
              (change)="$event ? selection.toggle(row) : null"
              [checked]="selection.isSelected(row)"
            />
          </td>
        </ng-container>
        
        <!-- Name Column -->
        <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>
        
        <!-- Email Column -->
        <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>
        
        <!-- Role Column -->
        <ng-container matColumnDef="role">
          <th mat-header-cell *matHeaderCellDef mat-sort-header>Role</th>
          <td mat-cell *matCellDef="let user">
            <mat-chip>{{ user.role }}</mat-chip>
          </td>
        </ng-container>
        
        <!-- Status Column -->
        <ng-container matColumnDef="status">
          <th mat-header-cell *matHeaderCellDef>Status</th>
          <td mat-cell *matCellDef="let user">
            <mat-chip [class.active]="user.status === 'active'">
              {{ user.status }}
            </mat-chip>
          </td>
        </ng-container>
        
        <!-- Actions Column -->
        <ng-container matColumnDef="actions">
          <th mat-header-cell *matHeaderCellDef>Actions</th>
          <td mat-cell *matCellDef="let user">
            <button mat-icon-button [matMenuTriggerFor]="menu">
              <mat-icon>more_vert</mat-icon>
            </button>
            <mat-menu #menu="matMenu">
              <button mat-menu-item (click)="edit(user)">
                <mat-icon>edit</mat-icon>
                <span>Edit</span>
              </button>
              <button mat-menu-item (click)="delete(user)">
                <mat-icon>delete</mat-icon>
                <span>Delete</span>
              </button>
            </mat-menu>
          </td>
        </ng-container>
        
        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
        
        <!-- No data row -->
        <tr class="mat-row" *matNoDataRow>
          <td class="mat-cell" [attr.colspan]="displayedColumns.length">
            No data matching "{{ searchInput.value }}"
          </td>
        </tr>
      </table>
      
      <mat-paginator
        [pageSizeOptions]="[5, 10, 25, 100]"
        showFirstLastButtons
      />
    </div>
  `
})
export class DataTableComponent implements AfterViewInit {
  displayedColumns = ['select', 'name', 'email', 'role', 'status', 'actions'];
  dataSource = new MatTableDataSource<User>();
  selection = new SelectionModel<User>(true, []);
  
  @ViewChild(MatPaginator) paginator!: MatPaginator;
  @ViewChild(MatSort) sort!: MatSort;
  
  users = input.required<User[]>();
  
  constructor() {
    effect(() => {
      this.dataSource.data = this.users();
    });
  }
  
  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }
  
  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();
    
    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }
  }
  
  isAllSelected(): boolean {
    const numSelected = this.selection.selected.length;
    const numRows = this.dataSource.data.length;
    return numSelected === numRows;
  }
  
  toggleAll() {
    this.isAllSelected()
      ? this.selection.clear()
      : this.dataSource.data.forEach(row => this.selection.select(row));
  }
  
  edit(user: User) { /* ... */ }
  delete(user: User) { /* ... */ }
}

Dialogs & Overlays

// dialog.service.ts
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';

@Injectable({ providedIn: 'root' })
export class DialogService {
  private dialog = inject(MatDialog);
  
  open<T, R = any>(
    component: ComponentType<T>,
    config?: MatDialogConfig
  ): Observable<R | undefined> {
    const dialogRef = this.dialog.open(component, {
      width: '500px',
      disableClose: false,
      autoFocus: true,
      ...config
    });
    
    return dialogRef.afterClosed();
  }
  
  confirm(message: string, title = 'Confirm'): Observable<boolean> {
    return this.open(ConfirmDialogComponent, {
      data: { title, message },
      width: '400px'
    });
  }
  
  alert(message: string, title = 'Alert'): Observable<void> {
    return this.open(AlertDialogComponent, {
      data: { title, message },
      width: '400px'
    });
  }
}

// confirm-dialog.component.ts
@Component({
  selector: 'app-confirm-dialog',
  standalone: true,
  imports: [MatDialogModule, MatButtonModule],
  template: `
    <h2 mat-dialog-title>{{ data.title }}</h2>
    
    <mat-dialog-content>
      <p>{{ data.message }}</p>
    </mat-dialog-content>
    
    <mat-dialog-actions align="end">
      <button mat-button [mat-dialog-close]="false">Cancel</button>
      <button mat-raised-button color="primary" [mat-dialog-close]="true">
        Confirm
      </button>
    </mat-dialog-actions>
  `
})
export class ConfirmDialogComponent {
  data = inject(MAT_DIALOG_DATA);
}

// user-form-dialog.component.ts
@Component({
  selector: 'app-user-form-dialog',
  standalone: true,
  imports: [
    MatDialogModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    ReactiveFormsModule
  ],
  template: `
    <h2 mat-dialog-title>
      {{ data.user ? 'Edit User' : 'Create User' }}
    </h2>
    
    <form [formGroup]="form" (ngSubmit)="save()">
      <mat-dialog-content>
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Name</mat-label>
          <input matInput formControlName="name" />
          <mat-error>Name is required</mat-error>
        </mat-form-field>
        
        <mat-form-field appearance="outline" class="full-width">
          <mat-label>Email</mat-label>
          <input matInput formControlName="email" type="email" />
          <mat-error>Valid email is required</mat-error>
        </mat-form-field>
      </mat-dialog-content>
      
      <mat-dialog-actions align="end">
        <button mat-button type="button" mat-dialog-close>Cancel</button>
        <button 
          mat-raised-button 
          color="primary" 
          type="submit"
          [disabled]="form.invalid"
        >
          Save
        </button>
      </mat-dialog-actions>
    </form>
  `
})
export class UserFormDialogComponent {
  private dialogRef = inject(MatDialogRef<UserFormDialogComponent>);
  data = inject<{ user?: User }>(MAT_DIALOG_DATA);
  
  form = inject(FormBuilder).group({
    name: [this.data.user?.name ?? '', Validators.required],
    email: [this.data.user?.email ?? '', [Validators.required, Validators.email]]
  });
  
  save() {
    if (this.form.valid) {
      this.dialogRef.close(this.form.value);
    }
  }
}

CDK Features

Drag and Drop

// kanban-board.component.ts
import { CdkDragDrop, CdkDropList, CdkDrag, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-kanban-board',
  standalone: true,
  imports: [CdkDropList, CdkDrag],
  template: `
    <div class="board" cdkDropListGroup>
      @for (column of columns(); track column.id) {
        <div class="column">
          <h3>{{ column.name }}</h3>
          
          <div
            cdkDropList
            [cdkDropListData]="column.tasks"
            (cdkDropListDropped)="drop($event)"
            class="task-list"
          >
            @for (task of column.tasks; track task.id) {
              <div cdkDrag class="task-card">
                <div class="drag-placeholder" *cdkDragPlaceholder></div>
                {{ task.title }}
              </div>
            }
          </div>
        </div>
      }
    </div>
  `,
  styles: [`
    .board {
      display: flex;
      gap: 16px;
    }
    .column {
      width: 300px;
      background: #f5f5f5;
      border-radius: 8px;
      padding: 16px;
    }
    .task-list {
      min-height: 100px;
    }
    .task-card {
      background: white;
      padding: 16px;
      border-radius: 4px;
      margin-bottom: 8px;
      cursor: move;
    }
    .cdk-drag-preview {
      box-shadow: 0 5px 20px rgba(0,0,0,0.2);
    }
    .cdk-drag-placeholder {
      background: #e0e0e0;
      border: 2px dashed #999;
      border-radius: 4px;
    }
    .cdk-drag-animating {
      transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
    }
  `]
})
export class KanbanBoardComponent {
  columns = input.required<Column[]>();
  taskMoved = output<{ task: Task; fromColumn: string; toColumn: string }>();
  
  drop(event: CdkDragDrop<Task[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
      
      this.taskMoved.emit({
        task: event.container.data[event.currentIndex],
        fromColumn: event.previousContainer.id,
        toColumn: event.container.id
      });
    }
  }
}

Virtual Scrolling

// virtual-list.component.ts
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-virtual-list',
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport 
      itemSize="50" 
      class="viewport"
      (scrolledIndexChange)="onScroll($event)"
    >
      <div 
        *cdkVirtualFor="let item of items(); 
                        let index = index;
                        trackBy: trackById"
        class="item"
      >
        <span>{{ index + 1 }}.</span>
        <span>{{ item.name }}</span>
        <span>{{ item.email }}</span>
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .viewport {
      height: 400px;
      width: 100%;
    }
    .item {
      height: 50px;
      display: flex;
      align-items: center;
      gap: 16px;
      padding: 0 16px;
      border-bottom: 1px solid #eee;
    }
  `]
})
export class VirtualListComponent {
  items = input.required<Item[]>();
  scrolled = output<number>();
  
  trackById(index: number, item: Item) {
    return item.id;
  }
  
  onScroll(index: number) {
    this.scrolled.emit(index);
  }
}

Overlay & Portal

// dropdown.component.ts
import { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';

@Component({
  selector: 'app-dropdown-trigger',
  template: `
    <button 
      #trigger
      (click)="toggle()"
      [attr.aria-expanded]="isOpen()"
    >
      {{ label() }}
      <mat-icon>arrow_drop_down</mat-icon>
    </button>
  `
})
export class DropdownTriggerComponent {
  private overlay = inject(Overlay);
  private viewContainerRef = inject(ViewContainerRef);
  @ViewChild('trigger') triggerRef!: ElementRef;
  
  label = input('Select');
  content = input.required<Type<any>>();
  
  private overlayRef: OverlayRef | null = null;
  isOpen = signal(false);
  
  toggle() {
    this.isOpen() ? this.close() : this.open();
  }
  
  open() {
    const positionStrategy = this.overlay
      .position()
      .flexibleConnectedTo(this.triggerRef)
      .withPositions([
        {
          originX: 'start',
          originY: 'bottom',
          overlayX: 'start',
          overlayY: 'top',
          offsetY: 4
        },
        {
          originX: 'start',
          originY: 'top',
          overlayX: 'start',
          overlayY: 'bottom',
          offsetY: -4
        }
      ]);
    
    const config: OverlayConfig = {
      positionStrategy,
      hasBackdrop: true,
      backdropClass: 'cdk-overlay-transparent-backdrop',
      scrollStrategy: this.overlay.scrollStrategies.reposition()
    };
    
    this.overlayRef = this.overlay.create(config);
    
    const portal = new ComponentPortal(this.content(), this.viewContainerRef);
    this.overlayRef.attach(portal);
    
    this.overlayRef.backdropClick().subscribe(() => this.close());
    this.isOpen.set(true);
  }
  
  close() {
    this.overlayRef?.dispose();
    this.overlayRef = null;
    this.isOpen.set(false);
  }
}

Theming

// custom-theme.scss
@use '@angular/material' as mat;
@use 'sass:map';

// Custom palette
$custom-primary: (
  50: #e3f2fd,
  100: #bbdefb,
  200: #90caf9,
  300: #64b5f6,
  400: #42a5f5,
  500: #2196f3,
  600: #1e88e5,
  700: #1976d2,
  800: #1565c0,
  900: #0d47a1,
  contrast: (
    50: rgba(black, 0.87),
    100: rgba(black, 0.87),
    200: rgba(black, 0.87),
    300: rgba(black, 0.87),
    400: rgba(black, 0.87),
    500: white,
    600: white,
    700: white,
    800: white,
    900: white,
  )
);

$my-primary: mat.m2-define-palette($custom-primary);
$my-accent: mat.m2-define-palette(mat.$m2-amber-palette, A200, A100, A400);

$my-theme: mat.m2-define-light-theme((
  color: (
    primary: $my-primary,
    accent: $my-accent,
  ),
  typography: mat.m2-define-typography-config(
    $font-family: 'Inter, sans-serif',
    $headline-1: mat.m2-define-typography-level(96px, 1.2, 300),
    $body-1: mat.m2-define-typography-level(16px, 1.5, 400),
  ),
  density: -1,
));

@include mat.all-component-themes($my-theme);

// Component-specific overrides
.mat-mdc-button {
  border-radius: 8px;
}

.mat-mdc-card {
  border-radius: 12px;
}

Best Practices

Import Only What You Need

Import individual modules to minimize bundle size

Use CDK for Custom Components

Build on CDK primitives instead of from scratch

Follow a11y Guidelines

Material components are accessible by default - don’t break them

Customize via Theming

Use Sass theming instead of CSS overrides

Next: PWA & Service Workers

Build offline-capable Progressive Web Apps with Angular