- Angular
Angular Materialの使い方|導入からテーブル・ダイアログまで
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期連続の黒字決算。