Angular Material とは、Angular チームが提供する UI コンポーネント集で、ボタン・入力欄・テーブル・ダイアログなどを Material Design(現在は Material 3)の見た目で使えるライブラリです。 ng add @angular/material の1コマンドで入り、使いたいコンポーネントを各コンポーネントの imports に足して使います。

この記事では、Angular 22.2.1 の standalone 構成(NgModule を書かない構成)で、ng add が実際に書き換えたファイルの差分から始めて、テーブル(ソート・ページング)、ダイアログ(値の受け渡し)、フォーム(エラー表示)をそのまま貼れる完成コードで載せます。どれもブラウザで操作した結果を付けています。後半では、mat.theme とシステムトークンによる色の変更とダークモード対応、import を忘れたときに出るエラー文と直し方をまとめます。

確認環境:Linux(x64)、Node.js 24.21.0、Angular 22.2.1 / Angular CLI 22.2.1、Angular Material 22.2.1・Angular CDK 22.2.1、ブラウザは Playwright 1.56.0 の Chromium。

Angular Material の早見表

導入から表示までの流れ

手順 すること
1 ng add @angular/material を実行し、色の組み合わせを選ぶ
2 使いたいコンポーネントのモジュールを、コンポーネントの imports に足す
3 テンプレートにタグや属性を書く(mat-table、matButton など)
4 色や角丸を変えたいときは src/styles.scss の mat.theme を編集する

よく使うコンポーネントと import 名

下の表の import は、すべて1つのコンポーネントにまとめて書き、Angular Material 22.2.1 でコンパイルが通ることを確かめた名前です。

用途 テンプレートでの書き方 import するもの import 元
ボタン matButton("filled" "outlined" "tonal" "elevated")、matIconButton、matFab MatButtonModule @angular/material/button
アイコン mat-icon MatIconModule @angular/material/icon
ツールバー mat-toolbar MatToolbarModule @angular/material/toolbar
カード mat-card mat-card-header mat-card-content mat-card-actions MatCardModule @angular/material/card
入力欄の枠 mat-form-field mat-label mat-hint mat-error MatFormFieldModule @angular/material/form-field
テキスト入力 matInput MatInputModule @angular/material/input
セレクト mat-select mat-option MatSelectModule @angular/material/select
チェックボックス mat-checkbox MatCheckboxModule @angular/material/checkbox
ラジオボタン mat-radio-group mat-radio-button MatRadioModule @angular/material/radio
スイッチ mat-slide-toggle MatSlideToggleModule @angular/material/slide-toggle
日付選択 mat-datepicker matDatepicker MatDatepickerModule と provideNativeDateAdapter() @angular/material/datepicker、@angular/material/core
入力候補 mat-autocomplete matAutocomplete MatAutocompleteModule @angular/material/autocomplete
テーブル mat-table matColumnDef mat-header-row mat-row MatTableModule @angular/material/table
ソート matSort mat-sort-header MatSortModule @angular/material/sort
ページング mat-paginator MatPaginatorModule @angular/material/paginator
ダイアログ mat-dialog-title mat-dialog-content mat-dialog-actions mat-dialog-close MatDialogModule(開く側は MatDialog を inject) @angular/material/dialog
通知(スナックバー) snackBar.open('文言', '閉じる') MatSnackBar を inject @angular/material/snack-bar
メニュー mat-menu matMenuTriggerFor mat-menu-item MatMenuModule @angular/material/menu
タブ mat-tab-group mat-tab MatTabsModule @angular/material/tabs
サイドナビ mat-sidenav-container mat-sidenav mat-sidenav-content MatSidenavModule @angular/material/sidenav
リスト mat-list mat-list-item MatListModule @angular/material/list
読み込み表示 mat-spinner、mat-progress-bar MatProgressSpinnerModule、MatProgressBarModule @angular/material/progress-spinner、@angular/material/progress-bar
ツールチップ matTooltip MatTooltipModule @angular/material/tooltip
バッジ matBadge MatBadgeModule @angular/material/badge
チップ mat-chip-set mat-chip MatChipsModule @angular/material/chips
開閉パネル mat-accordion mat-expansion-panel MatExpansionModule @angular/material/expansion
ステッパー mat-stepper mat-step MatStepperModule @angular/material/stepper

import 元は「@angular/material/ + コンポーネント名」、モジュール名は「Mat + 名前 + Module」という規則です。迷ったら、この規則で書いてエディターの補完に任せると早く見つかります。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular Materialの使い方|導入からテーブル・ダイアログまで」

ng add で導入する:質問項目と生成される差分

コマンドと質問

ng add @angular/material

Angular Material 22.2.1 の ng add が尋ねる質問は、色の組み合わせ(パレットの組)を選ぶ1問です。同梱されている定義ファイル(schematics/ng-add/schema.json)で確かめた選択肢は次の4つです。

選択肢 値
Azure/Blue(既定) azure-blue
Rose/Red rose-red
Magenta/Violet magenta-violet
Cyan/Orange cyan-orange

質問文は「Select a pair of starter prebuilt color palettes for your Angular Material theme」です。今回は、質問を既定値で進める --defaults と、インストール前の確認を省く --skip-confirmation を付けて実行しました。

ng add @angular/material --skip-confirmation --defaults
✔ Determining Package Manager
  › Using package manager: npm
✔ Searching for compatible package version
  › Found compatible package version: 22.2.1.
✔ Loading package information
✔ Installing package
UPDATE package.json (845 bytes)
✔ Packages installed successfully.
UPDATE src/styles.scss (1259 bytes)
UPDATE src/index.html (633 bytes)

書き換えられたファイル

git status で見ると、変わったのは package.json(と package-lock.json)、src/index.html、src/styles.scss の3か所です。angular.json と app.config.ts は変わりませんでした。

package.json には2つのパッケージが足されます。

   "dependencies": {
+    "@angular/cdk": "^22.2.1",
     "@angular/common": "^22.2.0",
     "@angular/compiler": "^22.2.0",
     "@angular/core": "^22.2.0",
     "@angular/forms": "^22.2.0",
+    "@angular/material": "^22.2.1",

src/index.html には、フォント(Roboto)とアイコン用フォントを読み込む link が足されます(追加された行のみ)。

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
  rel="stylesheet"
/>
<link
  href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined"
  rel="stylesheet"
/>

src/styles.scss には、テーマの定義が書き込まれます(コメントは省略)。

@use '@angular/material' as mat;

html {
  height: 100%;
  @include mat.theme(
    (
      color: (
        primary: mat.$azure-palette,
        tertiary: mat.$blue-palette,
      ),
      typography: Roboto,
      density: 0,
    )
  );
}

body {
  color-scheme: light;
  background-color: var(--mat-sys-surface);
  color: var(--mat-sys-on-surface);
  font: var(--mat-sys-body-medium);
  margin: 0;
  height: 100%;
}

この mat.theme が、Material 3 の色・文字・余白を CSS 変数として定義する部分です。後半のテーマの節で書き換えます。

導入直後のビルドで止まるとき

ng add の直後に ng build を実行したところ、次のメッセージで止まりました。

✖ Building... [FAILED: Inlining of fonts failed. https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap returned status code: 403.]

本番ビルドは、index.html にあるフォントの CSS を取りに行ってページへ埋め込みます。ビルドする環境から Google Fonts に届かない場合(社内ネットワークや CI など)に、この形で止まります。angular.json の production 構成で、フォントの埋め込みだけを無効にするとビルドが通りました。

"production": {
  "optimization": {
    "scripts": true,
    "styles": true,
    "fonts": false
  }
}

最小の例:ボタンとツールバー

standalone 構成では、使うコンポーネントのモジュールを、そのコンポーネントの imports に書きます。アプリ全体の画面枠になる App です(あとで出てくる3つのコンポーネントを並べています)。

// src/app/app.ts
import { Component, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { DialogDemo } from './dialog-demo';
import { TaskForm } from './task-form';
import { TaskTable } from './task-table';

@Component({
  selector: 'app-root',
  imports: [MatToolbarModule, MatButtonModule, TaskTable, DialogDemo, TaskForm],
  template: `
    <mat-toolbar>
      <span>タスク管理</span>
      <span class="spacer"></span>
      <button matButton id="dark" (click)="toggleDark()">{{ dark() ? 'ライト' : 'ダーク' }}に切り替え</button>
    </mat-toolbar>
    <main>
      <app-task-table />
      <app-dialog-demo />
      <app-task-form />
      <div class="accent-area"><button matButton="filled" id="accent-btn">上書きした色のボタン</button></div>
      <button matButton="filled" id="normal-btn">通常のボタン</button>
      <div class="note" id="note">システムトークンで色を付けた自作の枠</div>
    </main>
  `,
  styles: `
    .spacer { flex: 1; }
    main { padding: 16px; display: grid; gap: 24px; }
    .note {
      padding: 12px;
      border-radius: var(--mat-sys-corner-medium);
      background: var(--mat-sys-primary-container);
      color: var(--mat-sys-on-primary-container);
      font: var(--mat-sys-body-large);
    }
  `,
})
export class App {
  protected readonly dark = signal(false);

  protected toggleDark(): void {
    this.dark.update((v) => !v);
    document.documentElement.classList.toggle('dark-mode', this.dark());
  }
}

app.config.ts は ng new で作られたものから、使っていないルーターの設定を外しただけです。Angular Material のための provider は足していませんが、この記事のテーブル・ダイアログ・フォームはすべて動きました。

// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [provideBrowserGlobalErrorListeners()],
};

ボタンは matButton 属性で指定し、値で見た目を選びます。

書き方 見た目
matButton 文字だけ
matButton="filled" 塗りつぶし
matButton="outlined" 枠線
matButton="tonal" 淡い塗り
matButton="elevated" 影つき
matIconButton アイコンだけの丸いボタン
matFab 浮いて見える丸いボタン

テーブル:ソートとページングつきの完成コード

データはタスクの一覧にします。

// src/app/task.ts
export interface Task {
  id: number;
  title: string;
  owner: string;
  hours: number;
}

export const TASKS: Task[] = [
  { id: 1, title: '画面設計', owner: '佐藤', hours: 8 },
  { id: 2, title: 'API作成', owner: '鈴木', hours: 13 },
  { id: 3, title: '単体テスト', owner: '高橋', hours: 5 },
  { id: 4, title: '結合テスト', owner: '佐藤', hours: 21 },
  { id: 5, title: '手順書作成', owner: '田中', hours: 3 },
  { id: 6, title: 'レビュー', owner: '鈴木', hours: 2 },
  { id: 7, title: 'リリース準備', owner: '高橋', hours: 4 },
];

テーブル本体です。MatTableModule・MatSortModule・MatPaginatorModule の3つを import します。

// src/app/task-table.ts
import { AfterViewInit, Component, viewChild } from '@angular/core';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Task, TASKS } from './task';

@Component({
  selector: 'app-task-table',
  imports: [MatTableModule, MatSortModule, MatPaginatorModule],
  template: `
    <table mat-table [dataSource]="dataSource" matSort>
      <ng-container matColumnDef="id">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>ID</th>
        <td mat-cell *matCellDef="let task">{{ task.id }}</td>
      </ng-container>
      <ng-container matColumnDef="title">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>タスク</th>
        <td mat-cell *matCellDef="let task">{{ task.title }}</td>
      </ng-container>
      <ng-container matColumnDef="owner">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>担当</th>
        <td mat-cell *matCellDef="let task">{{ task.owner }}</td>
      </ng-container>
      <ng-container matColumnDef="hours">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>時間</th>
        <td mat-cell *matCellDef="let task">{{ task.hours }}</td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="columns"></tr>
      <tr mat-row *matRowDef="let row; columns: columns"></tr>
    </table>
    <mat-paginator [pageSizeOptions]="[3, 5, 10]" showFirstLastButtons />
  `,
})
export class TaskTable implements AfterViewInit {
  protected readonly columns = ['id', 'title', 'owner', 'hours'];
  protected readonly dataSource = new MatTableDataSource<Task>(TASKS);

  private readonly sort = viewChild.required(MatSort);
  private readonly paginator = viewChild.required(MatPaginator);

  ngAfterViewInit(): void {
    // ソートとページングは、データソースに渡して初めて効く
    this.dataSource.sort = this.sort();
    this.dataSource.paginator = this.paginator();
  }
}

書き方の要点です。

書くもの 役割
[dataSource]="dataSource" 表示するデータ。MatTableDataSource に配列を渡す
matColumnDef="名前" 列の定義。この名前を columns の配列に並べる
*matHeaderCellDef / *matCellDef="let task" 見出しセルとデータセルのテンプレート
mat-header-row / mat-row 行の定義。columns の順番で列が並ぶ
matSort と mat-sort-header テーブルにソート機能を付け、見出しをクリックできるようにする
mat-paginator ページ送りの部品。pageSizeOptions の先頭が最初の件数になる
dataSource.sort / dataSource.paginator この代入をして初めて、並べ替えとページ分けがデータに効く

viewChild.required(MatSort) は、テンプレートの中の matSort を取得するシグナルです。画面ができてから取得できるので、ngAfterViewInit で dataSource に渡しています。

実行結果

ブラウザで見出しとページ送りを操作し、表示された行を読み取った結果です([ ] 内はページ表示)。

初期表示: 1 画面設計 佐藤 8 | 2 API作成 鈴木 13 | 3 単体テスト 高橋 5  [1 – 3 of 7]
「時間」を1回クリック: 6 レビュー 鈴木 2 | 5 手順書作成 田中 3 | 7 リリース準備 高橋 4  [1 – 3 of 7]
「時間」を2回クリック: 4 結合テスト 佐藤 21 | 2 API作成 鈴木 13 | 1 画面設計 佐藤 8  [1 – 3 of 7]
次のページ: 3 単体テスト 高橋 5 | 7 リリース準備 高橋 4 | 5 手順書作成 田中 3  [4 – 6 of 7]

見出しを1回クリックで昇順、2回で降順になり、ソートは7件全体に対して行われてからページに分けられています(降順の2ページ目が 5・4・3 と続いている)。数値の列は数値として並びました(2、3、4 … 13、21 の順)。

ダイアログ:値を渡して結果を受け取る完成コード

ダイアログは、「中身のコンポーネント」と「開く側」の2つで作ります。タスク名を変更するダイアログを例にします。

中身のコンポーネント

// src/app/rename-dialog.ts
import { Component, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

export interface RenameDialogData {
  title: string;
}

@Component({
  selector: 'app-rename-dialog',
  imports: [MatDialogModule, MatButtonModule, MatFormFieldModule, MatInputModule],
  template: `
    <h2 mat-dialog-title>タスク名の変更</h2>
    <mat-dialog-content>
      <mat-form-field>
        <mat-label>タスク名</mat-label>
        <input matInput id="rename-input" [value]="title()" (input)="title.set($any($event.target).value)" />
      </mat-form-field>
    </mat-dialog-content>
    <mat-dialog-actions align="end">
      <button matButton mat-dialog-close id="cancel">キャンセル</button>
      <button matButton="filled" id="ok" [mat-dialog-close]="title()">保存</button>
    </mat-dialog-actions>
  `,
})
export class RenameDialog {
  // 開く側が data に入れた値を受け取る
  private readonly data = inject<RenameDialogData>(MAT_DIALOG_DATA);
  protected readonly dialogRef = inject(MatDialogRef<RenameDialog, string>);
  protected readonly title = signal(this.data.title);
}

開く側

// src/app/dialog-demo.ts
import { Component, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { RenameDialog, RenameDialogData } from './rename-dialog';

@Component({
  selector: 'app-dialog-demo',
  imports: [MatButtonModule],
  template: `
    <p id="current">現在のタスク名: {{ title() }}</p>
    <button matButton="outlined" id="open" (click)="open()">名前を変更</button>
  `,
})
export class DialogDemo {
  private readonly dialog = inject(MatDialog);
  protected readonly title = signal('画面設計');

  protected open(): void {
    const ref = this.dialog.open<RenameDialog, RenameDialogData, string>(RenameDialog, {
      width: '400px',
      data: { title: this.title() }, // ダイアログへ渡す値
    });

    // 閉じたときの値を受け取る(キャンセルや Esc のときは undefined)
    ref.afterClosed().subscribe((result) => {
      console.log(`[dialog] 閉じた結果: ${JSON.stringify(result)}`);
      if (result) {
        this.title.set(result);
      }
    });
  }
}

値の流れは次のとおりです。

向き 書き方
開く側 → ダイアログ dialog.open(コンポーネント, { data: … }) で渡し、ダイアログ側は inject(MAT_DIALOG_DATA) で受け取る
ダイアログ → 開く側 [mat-dialog-close]="値" を付けたボタン、または dialogRef.close(値) で返し、開く側は afterClosed() で受け取る

dialog.open の型引数は、順に「ダイアログのコンポーネント」「渡すデータの型」「返ってくる値の型」です。書いておくと、data の中身と afterClosed() の結果に型が付きます。

実行結果

ダイアログを3回開き、「保存」「キャンセル」「Esc キー」でそれぞれ閉じたときの結果です。

入力欄の初期値: 画面設計
[dialog] 閉じた結果: "画面設計(改訂)"
現在のタスク名: 画面設計(改訂)
[dialog] 閉じた結果: ""
[dialog] 閉じた結果: undefined
現在のタスク名: 画面設計(改訂)
閉じ方 afterClosed() に届いた値
[mat-dialog-close]="title()" のボタン(保存) 入力した文字列
値なしの mat-dialog-close のボタン(キャンセル) 空文字 ""
Esc キー undefined

キャンセルと Esc で届く値が違います。 どちらも「何もしない」にしたいので、開く側では if (result) と書いて、空文字と undefined の両方を除いています。「空文字での保存」も区別したい画面では、返す値を { saved: true, title } のようなオブジェクトにすると判定がはっきりします。

ダイアログの幅は、dialog.open の2つ目の引数の width で指定しています。

フォーム:エラー表示つきの完成コード

mat-form-field は、ラベル・補足・エラー文をまとめて表示する枠です。リアクティブフォームと組み合わせます。

// src/app/task-form.ts
import { Component, inject, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';

@Component({
  selector: 'app-task-form',
  imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="submit()">
      <mat-form-field appearance="outline">
        <mat-label>タスク名</mat-label>
        <input matInput formControlName="title" id="f-title" />
        <mat-hint>20文字まで</mat-hint>
        @if (form.controls.title.hasError('required')) {
          <mat-error>タスク名を入力してください</mat-error>
        } @else if (form.controls.title.hasError('maxlength')) {
          <mat-error>20文字以内で入力してください</mat-error>
        }
      </mat-form-field>

      <mat-form-field appearance="outline">
        <mat-label>担当</mat-label>
        <mat-select formControlName="owner" id="f-owner">
          @for (name of owners; track name) {
            <mat-option [value]="name">{{ name }}</mat-option>
          }
        </mat-select>
        @if (form.controls.owner.hasError('required')) {
          <mat-error>担当を選んでください</mat-error>
        }
      </mat-form-field>

      <mat-form-field appearance="outline">
        <mat-label>時間</mat-label>
        <input matInput type="number" formControlName="hours" id="f-hours" />
        @if (form.controls.hours.hasError('min')) {
          <mat-error>1以上を入力してください</mat-error>
        }
      </mat-form-field>

      <button matButton="filled" type="submit" id="f-submit">登録</button>
    </form>
    <p id="f-result">{{ result() }}</p>
  `,
  styles: `
    form { display: flex; flex-direction: column; gap: 8px; max-width: 320px; }
  `,
})
export class TaskForm {
  private readonly fb = inject(FormBuilder);
  protected readonly owners = ['佐藤', '鈴木', '高橋', '田中'];
  protected readonly result = signal('');

  protected readonly form = this.fb.nonNullable.group({
    title: ['', [Validators.required, Validators.maxLength(20)]],
    owner: ['', Validators.required],
    hours: [1, [Validators.required, Validators.min(1)]],
  });

  protected submit(): void {
    if (this.form.invalid) {
      this.form.markAllAsTouched(); // 未入力の欄にもエラーを出す
      return;
    }
    this.result.set(`登録: ${JSON.stringify(this.form.getRawValue())}`);
  }
}

要点は次のとおりです。

  • mat-form-field の中に、matInput を付けた input か mat-select を置く。枠だけでは入力欄になりません。
  • 何も触らずに登録ボタンを押したときは markAllAsTouched() を呼ぶ。全部の欄が「触れた」状態になり、未入力の欄にも mat-error が表示されます。
  • エラーの種類ごとの文言は hasError('required') などで出し分ける。@if / @else if で1つだけ表示されるようにしています。
  • fb.nonNullable.group を使うと、値の型から null が外れ、getRawValue() の結果をそのまま扱えます。

実行結果

空のまま登録: タスク名を入力してください / 担当を選んでください
21文字・0時間で登録: 20文字以内で入力してください / 担当を選んでください / 1以上を入力してください
登録: {"title":"設計レビュー","owner":"鈴木","hours":2}

1行目は、何も入力せずに登録ボタンを押したときに表示された mat-error の文言です。2行目では、タスク名のエラーが「入力してください」から「20文字以内で」に切り替わっています。3行目は、すべて正しく入力して登録したときの値です。

テーマ:mat.theme とシステムトークンで色を変える

mat.theme の引数

src/styles.scss の mat.theme に渡す設定は3つです。

キー 内容 ng add が書いた値
color 色。primary(主役の色)と tertiary(差し色)にパレットを指定 mat.$azure-palette と mat.$blue-palette
typography フォント Roboto
density 部品の詰まり具合 0

同梱されているパレットは、パッケージ内の定義ファイルで確かめたところ次の12種類です。

パレット(mat.$名前-palette)
red green blue yellow cyan magenta orange chartreuse spring-green azure violet rose

色を変え、ダークモードに対応した styles.scss

主役の色を violet に変え、ダークモードの切り替えと、一部だけ色を上書きする例を加えた完成形です。

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

html {
  height: 100%;
  color-scheme: light; // 既定はライト
  @include mat.theme(
    (
      color: (
        primary: mat.$violet-palette,
        tertiary: mat.$orange-palette,
      ),
      typography: Roboto,
      density: 0,
    )
  );
}

// ボタンで切り替える: html に dark-mode クラスを付けるとダーク
html.dark-mode {
  color-scheme: dark;
}

// OS の設定に合わせる: html に auto-mode クラスを付ける
html.auto-mode {
  color-scheme: light dark;
}

// 一部の領域だけトークンを上書きする
.accent-area {
  @include mat.theme-overrides(
    (
      primary: #00695c,
      on-primary: #ffffff,
    )
  );
}

body {
  background-color: var(--mat-sys-surface);
  color: var(--mat-sys-on-surface);
  font: var(--mat-sys-body-medium);
  margin: 0;
  height: 100%;
}

システムトークンとは

mat.theme は、--mat-sys- で始まる CSS 変数(システムトークン)を定義します。Angular Material のコンポーネントはこの変数を参照して色を決めているので、自作の部品でも同じ変数を使えば、テーマと色がそろいます。この記事のコードで使っているトークンです。

トークン 用途
--mat-sys-surface / --mat-sys-on-surface ページの背景と、その上の文字
--mat-sys-primary / --mat-sys-on-primary 主役の色と、その上の文字(塗りつぶしボタンなど)
--mat-sys-primary-container / --mat-sys-on-primary-container 主役の色の淡い版と、その上の文字
--mat-sys-corner-medium 角丸の大きさ
--mat-sys-body-medium / --mat-sys-body-large 本文用の文字設定(font にそのまま指定できる)

App の .note は、このトークンだけで色を付けた自作の枠です。

実行結果:ライト/ダークで色がどう変わるか

ブラウザで計算後の背景色を読み取りました。

状態 body の背景 通常の塗りつぶしボタン 上書きした領域のボタン 自作の枠
ライト(既定) rgb(254, 248, 252) rgb(125, 0, 250) rgb(0, 105, 92) rgb(236, 220, 255)
html に dark-mode クラス rgb(21, 19, 22) rgb(213, 186, 255) rgb(0, 105, 92) rgb(95, 0, 192)
auto-mode クラス+OS がダーク rgb(21, 19, 22) rgb(213, 186, 255) rgb(0, 105, 92) rgb(95, 0, 192)
auto-mode クラス+OS がライト rgb(254, 248, 252) rgb(125, 0, 250) rgb(0, 105, 92) rgb(236, 220, 255)

(OS の設定は、ブラウザの prefers-color-scheme を切り替えて再現しています。)

このときの --mat-sys-primary の中身は、次のようになっていました。

light-dark(#7d00fa, #d5baff)

トークンの値が CSS の light-dark() 関数で「ライト用, ダーク用」の2色を持っているので、color-scheme を切り替えるだけで全体の色が入れ替わります。ダークモード用の CSS を別に書く必要はありません。

やりたいこと 書き方
常にライト color-scheme: light;
常にダーク color-scheme: dark;
OS の設定に合わせる color-scheme: light dark;
ボタンで切り替える クラスの有無で color-scheme を変える(App の toggleDark())

一部だけ色を変える:mat.theme-overrides

mat.theme-overrides は、囲んだ範囲のトークンを上書きします。上の表で「上書きした領域のボタン」が、ライトでもダークでも rgb(0, 105, 92)(#00695c)のままなのは、primary を1色で指定したためです。

import 漏れで出る「is not a known element」と直し方

standalone 構成でいちばん出会いやすいのが、imports への書き忘れです。実際に1つずつ外してビルドしたときのメッセージを載せます。

タグを書いたのにモジュールを import していない

MatPaginatorModule を外した場合です。

✘ [ERROR] NG8001: 'mat-paginator' is not a known element:
1. If 'mat-paginator' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'mat-paginator' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

    src/app/task-table.ts:32:4:
      32 │     <mat-paginator [pageSizeOptions]="[3, 5, 10]" showFirstLastBut...

直し方は、メッセージの1にあるとおり、そのコンポーネントの imports に MatPaginatorModule を足すことです。2で案内されている CUSTOM_ELEMENTS_SCHEMA は、Angular 以外の Web Component を使うとき向けの設定で、Angular Material のタグでは 1 の対応をします。

属性で使う部品のモジュールを import していない

MatTableModule を外した場合です。mat-table は table タグに付ける属性なので、メッセージが変わります。

✘ [ERROR] NG8002: Can't bind to 'dataSource' since it isn't a known property of 'table'.

ビルドは通り、ブラウザで分かるもの

MatInputModule を外した場合は、ビルドが成功しました。matInput は input タグのふつうの属性として扱われるため、コンパイル時には気づけません。ブラウザで開くと、コンソールに次のエラーが出て画面が表示されませんでした(本番ビルドでの表示)。

ERROR TypeError: Cannot read properties of undefined (reading 'controlType')

mat-form-field が、中にあるはずの入力部品(matInput や mat-select)を見つけられなかったときの症状です。

エラーと直し方の一覧

出たメッセージ 原因 直し方
NG8001: 'mat-paginator' is not a known element タグのモジュールが imports に無い 早見表の import 名を、そのタグを書いたコンポーネントの imports に足す
NG8002: Can't bind to 'dataSource' since it isn't a known property of 'table' 属性で使う部品(mat-table)のモジュールが無い MatTableModule を足す
NG8002: Can't bind to 'pageSizeOptions' since it isn't a known property of 'mat-paginator' NG8001 と同時に出る NG8001 を直せば消える
ブラウザで Cannot read properties of undefined (reading 'controlType') mat-form-field の中の入力部品が有効になっていない MatInputModule(または MatSelectModule)を足す
Inlining of fonts failed. … returned status code: 403. ビルド環境からフォントの配信元に届かない production 構成の optimization で "fonts": false
bundle initial exceeded maximum budget. Budget 1.00 MB was not met by 138.50 kB with a total of 1.14 MB. 初期バンドルが angular.json の budgets を超えた(早見表の部品を1画面に全部入れたときに出た) 画面ごとに遅延読み込みにする、または budgets の値を見直す

「import は、タグを書いたコンポーネントごとに必要」という点が、NgModule 構成との大きな違いです。親で import していても、子のテンプレートには効きません。ダイアログの中身(RenameDialog)にも、MatDialogModule や MatButtonModule を書いているのはそのためです。

よくある質問

Q. provideAnimations() などの設定は必要ですか? A. 今回は app.config.ts に Angular Material 用の provider を何も足さずに、テーブル・ダイアログ・セレクト・フォームが動きました。ng add も app.config.ts を書き換えていません。

Q. テーブルのソートが効きません。 A. matSort をテーブルに、mat-sort-header を見出しに付けたうえで、dataSource.sort = this.sort() の代入が必要です。この記事の例では、matColumnDef の名前をデータのプロパティ名と同じにしています。

Q. ダイアログを閉じたときの結果が undefined になります。 A. Esc キーや外側のクリックで閉じたときは undefined が届きます。ボタンで値を返すには、[mat-dialog-close]="値" のように角かっこ付きで書きます。値なしの mat-dialog-close では空文字が届きます。

Q. 色をコーポレートカラーに合わせたいときは? A. まずは12種類のパレットから近いものを primary に選びます。細かく合わせたい箇所は mat.theme-overrides でトークンを上書きします。上書きは範囲を絞れるので、ヘッダーだけ、特定のカードだけ、といった指定ができます。

まとめ

  • 導入は ng add @angular/material の1コマンド。質問は色の組み合わせの1問で、package.json・index.html・styles.scss が書き換わる
  • standalone 構成では、使う部品のモジュールを各コンポーネントの imports に書く。import 元は @angular/material/名前
  • テーブルは MatTableDataSource に sort と paginator を代入して、ソートとページングを効かせる
  • ダイアログは data で渡し、afterClosed() で受け取る。キャンセルは空文字、Esc は undefined が届く
  • フォームのエラーは mat-error と hasError()。未入力のまま送信されたら markAllAsTouched()
  • テーマは mat.theme とシステムトークン。color-scheme を切り替えるだけでダークモードになる
  • 「is not a known element」は imports の書き忘れ。メッセージに出たタグのモジュールを足す

参考・出典

  • Angular Material 22.2.1 同梱の schematics/ng-add/schema.json(ng add の質問と選択肢)、パレット定義、mat.theme / mat.theme-overrides の定義
  • ng add が styles.scss に書き込むコメント内の案内先:テーマ(https://material.angular.dev/guide/theming)、システムトークン(https://material.angular.dev/guide/system-variables)
  • ng build(Angular CLI 22.2.1)が出力したエラーメッセージ(NG8001、NG8002 ほか)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular Materialの使い方|導入からテーブル・ダイアログまで」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?