Documentation Index Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
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.
Think of Angular Material as two distinct layers. The CDK is like a chassis — it provides the mechanical behavior (drag-and-drop physics, focus management, scroll virtualization, overlay positioning) without any visual opinion. Angular Material is the body that sits on top — it adds Material Design styling to those behaviors. You can use the CDK without Material if you want custom-branded components that still have professional-grade behavior like keyboard navigation and screen reader support.
Practical decision : If your company has its own design system, use the CDK directly and skip Angular Material’s visual layer. If you need to ship fast and Material Design is acceptable, use Angular Material components as-is and customize via theming. Trying to make Material components look like a completely different design system (e.g., overriding 50+ CSS rules per component) is almost always more work than building on CDK from scratch.
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 -- the ng add schematic does the tedious setup
# that would otherwise take 15 minutes of manual configuration
ng add @angular/material
# This will:
# 1. Install @angular/material, @angular/cdk, @angular/animations
# 2. Add a pre-built theme to angular.json (you can swap later)
# 3. Add global typography and Roboto font imports
# 4. Set up animations provider (required for most Material components)
Common gotcha : If you skip ng add and just npm install @angular/material, you will miss the theme setup, font imports, and animation provider. Your components will render but look completely unstyled — no colors, no elevation, no transitions. Always use ng add for the initial setup.
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
Material provides several button variants, each designed for a specific level of visual emphasis. Think of it like typography: mat-button is body text (low emphasis), mat-raised-button is a subheading (medium emphasis), and mat-fab is a headline (high emphasis). Picking the right variant is not just aesthetics — it guides the user’s eye to the most important action on screen.
// 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 );
}
Material’s form fields are more than styled inputs — they handle the entire lifecycle of a form control: floating labels, hint text, error messages, prefix/suffix icons, and character counts. The mat-form-field wrapper is what ties all of these together. One important design choice: always use appearance="outline" for forms where users enter data (it provides the clearest affordance), and reserve appearance="fill" for filter bars or search inputs where density matters more.
// 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
MatTable is one of Material’s most powerful components, but also one of its most misunderstood. The key insight: MatTableDataSource is not just a wrapper around an array — it provides built-in filtering, sorting, and pagination that work out of the box when you wire up the corresponding directives. For most CRUD tables, you will never need to write custom filter or sort logic.
Practical tip : Use MatTableDataSource for tables with fewer than 1,000 rows. For larger datasets, use server-side pagination and sorting instead — pass the parameters to your API and let the database handle the heavy lifting. Client-side sorting of 10,000 rows will freeze the UI for hundreds of milliseconds.
// 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
Dialogs are one of the most over-used patterns in web apps. Before reaching for a dialog, ask: “Could this be inline?” Confirmation for a delete? Inline. Editing a single field? Inline. A multi-step wizard that needs the user’s full attention? That is a dialog. The rule of thumb: dialogs are for actions that need isolation from the rest of the page — they force a decision before the user can continue.
// 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
CDK’s drag-and-drop system handles the physics (momentum, reorder animation), accessibility (keyboard reordering), and cross-container transfers. You provide the data model and the visual template — the CDK handles the rest. The most important concept is cdkDropListGroup: it tells the CDK that multiple drop lists are connected, enabling items to move between them (like a Kanban board).
// 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 is the CDK’s answer to the “render 10,000 items without crashing the browser” problem. Instead of creating DOM nodes for every item (which would mean 10,000+ elements in the DOM), it only renders the items visible in the viewport plus a small buffer. As the user scrolls, old items are recycled and new ones are created. The result: a list of 100,000 items feels just as smooth as a list of 20.
Key constraint : Fixed-size virtual scrolling requires that every item has the exact same height (specified via itemSize). If your items have variable heights, you need the experimental autosize strategy, which is significantly more complex and less performant. Design your list items to be fixed-height whenever possible.
// 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
The CDK Overlay system is what powers every Material popup — dialogs, tooltips, select dropdowns, menus. It solves two hard problems: positioning (the dropdown should appear below the trigger, but flip above if there is not enough room) and z-index management (overlays should stack in creation order without you hardcoding z-index values). If you are building any custom popup component, use the CDK Overlay instead of rolling your own — you will avoid dozens of edge cases around scroll positioning, viewport boundaries, and backdrop handling.
// 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 ( 96 px , 1.2 , 300 ),
$body-1: mat . m2-define-typography-level ( 16 px , 1.5 , 400 ),
),
density: -1 ,
)) ;
@include mat . all-component-themes ( $my-theme );
// Component-specific overrides
.mat-mdc-button {
border-radius : 8 px ;
}
.mat-mdc-card {
border-radius : 12 px ;
}
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