Quick answer: Angular Material is the official component library for Angular, giving you accessible, themeable UI blocks like tables, form fields, dialogs and buttons. Install it with ng add @angular/material, pick a prebuilt theme or define your own with Sass tokens, and import components from @angular/material modules or standalone import arrays. Every component follows Material Design’s accessibility rules out of the box.

Quick answer card explaining what Angular Material is in plain language.
Angular Material roadmap listing the sections in reading order.

Installing and configuring Angular Material

The scaffolding command does more than add dependencies. ng add @angular/material installs the package, sets the project’s styles configuration, enables animations, and asks which prebuilt theme to use. Answer the prompts once and the CLI writes every setting for you, which is why a fresh Material project works the moment it runs.

ng add @angular/material
# chooses: Indigo/Pink theme, typography: Roboto, animations: yes

What the schematics actually touch matters when you upgrade later. The theme import lands in styles.css, the Roboto and Material Icons fonts are added to index.html, and provideAnimations() (or provideAnimationsAsync()) is registered in your app config. Standalone projects use the app.config.ts provider array; NgModule projects register it in AppModule. If animations are missing, every component that depends on them fails at runtime with an obscure “Found the synthetic property @transform” error, so check the provider first whenever Material behaves strangely.

Themes: prebuilt, dark mode and custom palettes

Prebuilt themes are single CSS files you import once: they set a primary palette, an accent palette, a warn palette and a density. Swapping to dark mode used to mean importing a different file; since Material v15 the system uses CSS custom properties, so one theme definition serves both modes and a media query or class toggle flips the values.

@use '@angular/material' as mat;

html {
  @include mat.core();

  $app-primary: mat.m2-define-palette(mat.$m2-indigo-palette);
  $app-accent:  mat.m2-define-palette(mat.$m2-pink-palette, A200);
  $app-warn:    mat.m2-define-palette(mat.$m2-red-palette);

  @include mat.all-component-themes((
    color: mat.m2-define-light-theme((
      primary: $app-primary,
      accent: $app-accent,
      warn: $app-warn,
    )),
  ));
}

/* dark mode: flip the scheme, keep the same palettes */
[data-theme='dark'] {
  @include mat.all-component-themes((
    color: mat.m2-define-dark-theme((
      primary: $app-primary,
      accent: $app-accent,
      warn: $app-warn,
    )),
  ));
}

Two details separate configurations that hold up from ones that break at scale. First, define palettes once and reuse them across components, so a button, a checkbox and a slide-toggle never disagree about the brand colour. Second, keep the dark block after the light block in the stylesheet, because both emit the same CSS custom properties and source order decides which wins. For the older pre-Material-3 theming API (v14 and earlier), the same structure applies with mat.define-light-theme and mat.all-theme-mixins instead.

Concept map of Angular Material: install, theme, components and accessibility.

Core components in practice: form field, table and dialog

Most applications converge on the same handful of components. The form field wraps any native input and adds labels, hints, error states and prefixes; the table handles sorting, pagination and row selection; the dialog opens content as a component with typed data in and a typed result out.

<mat-form-field appearance="outline">
  <mat-label>Email</mat-label>
  <input matInput [formControl]="email" />
  @if (email.invalid && email.touched) {
    <mat-error>Enter a valid address</mat-error>
  }
</mat-form-field>

// dialog with a typed contract
const ref = dialogRef.open(ConfirmDialogComponent, {
  data: { title: 'Delete project?' },
});
ref.afterClosed().subscribe((confirmed: boolean) => {
  if (confirmed) this.delete();
});

The dialog pattern deserves special attention because it is where most teams lose type safety. Pass a data object the child declares as @Input() via MAT_DIALOG_DATA, and close with a typed result instead of reaching back into the parent. For tabular data, prefer mat-table with a MatTableDataSource for local data, or manage the datasource yourself when rows stream in from an API — the component only cares about an observable of rows. The older narrow guides on this site, the scrollable table setup and the responsive grid recipe, still apply on top of these fundamentals.

Typography scales and layout with Angular CDK

Angular Material ships on the CDK, the component dev kit, which holds the unstyled primitives underneath. Two CDK pieces earn their place in almost every Material app: the layout utilities, which mirror CSS flexbox and grid as directives, and the typography configuration, which maps every heading, body and button text to a scale you define once.

import { LayoutModule } from '@angular/cdk/layout';
import {MatSidenavModule} from '@angular/material/sidenav';

// responsive side nav: handset gets a drawer, wide screen gets fixed nav
<mat-sidenav-container>
  <mat-sidenav [mode]="isHandset ? 'over' : 'side'" [opened]="!isHandset">
    <app-nav-items />
  </mat-sidenav>
  <mat-sidenav-content><router-outlet /></mat-sidenav-content>
</mat-sidenav-container>

The BreakpointObserver from the CDK answers the question every responsive layout eventually asks: is this a handset? Query Breakpoints.Handset and switch the sidenav mode, the table display, or the dialog size from that single signal. Because these are plain observables, they compose with the async pipe and with the signal-based state patterns covered in the signals guide.

Accessibility that comes built in

  • Focus management. Dialogs and menus move focus into themselves on open and restore it on close, which is what keyboard and screen-reader users expect.
  • Keyboard interaction. Every interactive component implements the ARIA authoring patterns for its role: menus respond to arrow keys, tabs to left and right arrows, checkboxes to space.
  • Semantics. Form fields wire <label> association automatically, tables use proper <th> markup, and live regions announce async status changes.
  • High contrast. The theming tokens feed forced-colors mode, so the OS high-contrast setting restyles components without extra CSS.

These guarantees are the strongest argument for Material over rolling your own components: the accessibility work is done, tested and maintained. Your responsibility is not to undo it — avoid replacing semantic elements with generic divs, keep the ARIA attributes the components emit, and test with a keyboard before assuming the defaults survived your custom CSS.

Performance: lazy loading Material modules

Importing MatTableModule everywhere bloats the initial bundle even when the table only appears on one route. The fix follows standard Angular code splitting: load the feature module lazily and import Material inside it, so the component code ships with the route. With standalone components the boundary is even cleaner — each route’s loadComponent chunk carries only the Material components that route imports.

// routes: the table chunk loads only when the route is visited
{
  path: 'reports',
  loadComponent: () =>
    import('./reports/reports.component').then(m => m.ReportsComponent),
}

Also prefer provideAnimationsAsync() over the synchronous provider. It loads the animations module behind a lazy chunk, saving roughly 80 kB of gzipped code from the initial payload while keeping every Material animation working. For dense lists, pair mat-list with CDK virtual scrolling so only visible rows exist in the DOM.

Common pitfalls and how to avoid them

The synthetic property error means animations were never provided, not that the component is broken. The datepicker showing the wrong locale means the locale data was never imported — call registerLocaleData before bootstrapping. A table whose columns vanish after an upgrade usually means the column IDs and the displayed columns array drifted apart, which is a data problem rather than a rendering one.

The bigger discipline is resisting the urge to fight the components. Deep selector overrides of Material internals break silently at every minor release, because the DOM structure is not a public API. Style through the documented theming tokens, extend components by wrapping them in your own, and keep a single upgrade commit that runs the schematics, so theme API changes are caught by the compiler rather than by users. To learn Angular properly with a mentor, Ampersand Academy teaches web development one-to-one.

Coverage grid showing every section covered in this Angular Material article.

Sorting, pagination and filtering on mat-table

The table is the component teams reach for first and the one they fight with longest, because the MatTableDataSource arrives with almost everything wired and understanding which wire does what takes a deliberate pass. Sorting and pagination are directives you add to the template and configure on the datasource; filtering is a plain string match you own. None of the three touches the server, so the moment rows come from an API you take over the datasource to feed it.

// client-side: directives + MatTableDataSource does everything
dataSource = new MatTableDataSource(this.rows);
displayedColumns = ['name', 'role', 'status'];

// in ngOnInit, after columns exist
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;

// filter applies to every column by default
applyFilter(event: Event) {
  const value = (event.target as HTMLInputElement).value;
  this.dataSource.filter = value.trim().toLowerCase();
}

Server-side pagination inverts the model. The table stops owning rows and becomes a view over an observable: each paginator or sort change becomes a request, and a switchMap cancels the previous one. Keeping the page state in query params rather than component state is what lets a refresh or a shared link restore the exact view, which users notice immediately once they have it.

Form fields, validation and error display

MatInput works with both reactive and template-driven forms, but the error display contract is worth learning precisely: mat-error shows only when the control is invalid and touched, which is the behaviour every good form wants and cannot be replicated with a plain div without extra flags. The form field also owns the hint text lifecycle, showing it by default and hiding it while an error displays, so the layout never shows both at once.

<mat-form-field appearance="outline">
  <mat-label>Password</mat-label>
  <input matInput type="password" [formControl]="password" />
  <mat-hint>At least 10 characters</mat-hint>
  @if (password.hasError('minlength')) {
    <mat-error>Password is too short</mat-error>
  }
</mat-form-field>

Select and autocomplete deserve the same care. For option lists that come from an API, use the panel’s filtering event rather than rebuilding the options array on every keystroke, because re-rendering the list mid-interaction loses the keyboard focus position. And when a form must show a server-side validation error against a specific field, map the server’s error envelope onto the control with setErrors rather than rendering a banner, so the error appears exactly where the user must fix it.

Upgrading without breaking your theme

Material’s theming API has moved twice in recent years, and an upgrade that ignores it compiles fine while silently dropping your palette. The schematics handle most of the migration: run the Angular update first, which brings the framework and Material in lockstep, then the theme migration, which rewrites the Sass to the current API and flags every deprecated mixin. What the tooling cannot do is decide your dark-mode strategy under the new token system, so plan that as a separate commit.

Two habits keep upgrades boring. Commit the generated theme output alongside the source so a visual diff catches palette drift in review, and keep a screenshot test of one page per component family — form, table, dialog, nav — so the first render after an upgrade is compared mechanically rather than eyeballed. The components most likely to change are the ones whose DOM you have customised most, which is a fair summary of the entire cost of deep overrides and the reason the wrapper-component pattern from the pitfalls section pays for itself at exactly this moment.

One more upgrade habit worth stealing: keep a branch that contains nothing but the upgrade, with no feature work mixed in. Framework upgrades occasionally change behaviour rather than just APIs, and bisecting a combined branch to find which change caused a regression costs far more than the discipline of landing the upgrade alone, reviewing its visual diff, and continuing features on top of a known-good base. Teams that adopt this rarely need emergency patches after upgrades, because the upgrade itself was the reviewable event.

The same reasoning applies to adding Material to an existing application. Resist importing the whole component library into the shared module on day one. Start with the two or three components the current screen actually needs, keep their imports inside the feature route, and let the bundle report tell you what each new component costs. Teams that onboard Material incrementally almost always end with a smaller initial payload than teams that import everything once, because the incremental path forces a conversation at each addition that the bulk import quietly skips.

New to Angular? Build the fundamentals first in the free 14-part Angular course, then come back to this guide for the deeper material.

Angular Material and the CDK: responsive layouts

Angular Material keeps layout behaviour in the CDK instead of the component layer, so responsive design starts with @angular/cdk/layout rather than a stack of media queries. BreakpointObserver exposes the same named breakpoints the components use — Handset, Tablet and Web — as an observable you inject once and reuse. One shared definition of what counts as a small screen keeps an Angular Material page consistent: the template, the drawer and the table all read the same state.

import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { map } from 'rxjs/operators';

readonly isHandset$ = this.breakpoint
  .observe([Breakpoints.HandsetPortrait])
  .pipe(map(result => result.matches));

Bind the result with the async pipe and the template owns the decision: a handset opens a drawer, a wide screen shows the fixed side navigation. The same pattern scales across Angular Material surfaces — a table that becomes a card list on small screens, a toolbar that collapses its actions — without a single matchMedia call inside a component.

Positioning is the other half of responsive behaviour, and Angular Material delegates it to the CDK overlay too. Menus, dialogs, tooltips and autocomplete panels render into the overlay container and reposition against scroll and resize, which is why a menu opened on the last row of a table flips upwards when the viewport runs out of room. Custom surfaces can import OverlayModule and reuse the connected positioning strategy instead of writing absolute coordinates; the backdrop, escape handling and accessibility metadata that Angular Material attaches come along with it.

Two habits keep a responsive Angular Material codebase honest. Prefer the breakpoint observable in a shared service over per-component subscriptions so the whole app agrees on the current mode, and check drawer and dialog flows at handset width before merging: the CDK focus trap keeps keyboard users inside them, but only when the component rendered as Angular Material expects.

Density closes the loop. Angular Material ships density tokens that compress control heights for data-heavy dashboards, and combining them with the breakpoint observer lets a screen trade comfort for fit only on small devices — an Angular Material layout that adapts in both directions instead of only hiding things.

The CDK also carries the accessibility primitives these layouts need. LiveAnnouncer narrates asynchronous table updates for screen readers, and the focus trap keeps keyboard users inside the drawer the breakpoint logic just opened, so the responsive behaviour and the accessibility guarantees arrive together rather than as two separate workstreams.

None of this replaces CSS. Media queries still own what a container looks like; the breakpoint observable owns which version of a component the app renders. Angular Material works best when that split stays clear: style in the stylesheet, structure in the template, and the CDK deciding which of the two the current screen needs.

Frequently asked questions

Is Angular Material free to use?

Yes. Angular Material is MIT-licensed and maintained by the Angular team itself, so it follows Angular releases and receives accessibility and compatibility updates at no cost.

How do I switch Angular Material to dark mode?

Define both a light and a dark theme with the theming mixins and gate the dark one behind a class or data attribute on the html element. Modern Material themes use CSS custom properties, so both modes can share the same palettes.

Why does my Datepicker show the wrong language?

Angular ships locale data lazily. Import the locale with registerLocaleData and set the LOCALE_ID provider, or pass the locale directly to MAT_DATE_LOCALE, before the Datepicker renders.

Can I use Angular Material without its fonts and icons?

Yes, but some components assume them. Material Icons are required by font ligature icon names; if you cannot load the font, use SVG icons with MatIconRegistry instead. The Roboto font is optional and only affects the default typography look.

Should I use Angular Material or Tailwind with Angular?

They solve different problems and combine well: Material supplies accessible interactive components, Tailwind supplies utility layout styles. Choose Material-first when accessibility and out-of-the-box behaviour matter most; choose a utility framework when the design system is fully custom.

Last updated on · Written by