diff --git a/src/material/button/_m2-icon-button.scss b/src/material/button/_m2-icon-button.scss
index f6a420e052d6..694034a1e246 100644
--- a/src/material/button/_m2-icon-button.scss
+++ b/src/material/button/_m2-icon-button.scss
@@ -7,6 +7,8 @@
$system: m2-utils.get-system($theme);
$density-scale: theming.clamp-density(map.get($system, density-scale), -3);
$touch-target-display: block;
+ $disabled: m3-utils.color-with-opacity(map.get($system, on-surface), 38%);
+ $disabled-container: m3-utils.color-with-opacity(map.get($system, on-surface), 12%);
@if ($density-scale < -1) {
$touch-target-display: none;
@@ -19,8 +21,30 @@
icon-button-touch-target-size: 48px,
),
color: (
- icon-button-disabled-icon-color:
- m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
+ icon-button-container-color: transparent,
+ icon-button-disabled-icon-color: $disabled,
+ icon-button-filled-container-color: map.get($system, surface),
+ icon-button-filled-disabled-container-color: $disabled-container,
+ icon-button-filled-disabled-icon-color: $disabled,
+ icon-button-filled-disabled-state-layer-color: map.get($system, on-surface-variant),
+ icon-button-filled-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
+ icon-button-filled-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
+ icon-button-filled-icon-color: map.get($system, on-surface),
+ icon-button-filled-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
+ icon-button-filled-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-surface), map.get($system, pressed-state-layer-opacity)),
+ icon-button-filled-state-layer-color: map.get($system, on-surface),
+ icon-button-tonal-container-color: map.get($system, surface),
+ icon-button-tonal-disabled-container-color: $disabled-container,
+ icon-button-tonal-disabled-icon-color: $disabled,
+ icon-button-tonal-disabled-state-layer-color: map.get($system, on-surface-variant),
+ icon-button-tonal-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
+ icon-button-tonal-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
+ icon-button-tonal-icon-color: map.get($system, on-surface),
+ icon-button-tonal-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
+ icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-surface), map.get($system, pressed-state-layer-opacity)),
+ icon-button-tonal-state-layer-color: map.get($system, on-surface),
icon-button-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
@@ -51,6 +75,16 @@
$system: m3-utils.replace-colors-with-variant($system, primary, $color-variant);
@return (
+ icon-button-filled-container-color: map.get($system, primary),
+ icon-button-filled-icon-color: map.get($system, on-primary),
+ icon-button-filled-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
+ icon-button-filled-state-layer-color: map.get($system, on-primary),
+ icon-button-tonal-container-color: map.get($system, primary),
+ icon-button-tonal-icon-color: map.get($system, on-primary),
+ icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
+ icon-button-tonal-state-layer-color: map.get($system, on-primary),
icon-button-icon-color: map.get($system, primary),
icon-button-state-layer-color: map.get($system, primary),
icon-button-ripple-color: m3-utils.color-with-opacity(
diff --git a/src/material/button/_m3-icon-button.scss b/src/material/button/_m3-icon-button.scss
index 5639ca2a227c..9eef65f4bb94 100644
--- a/src/material/button/_m3-icon-button.scss
+++ b/src/material/button/_m3-icon-button.scss
@@ -23,8 +23,35 @@
icon-button-touch-target-size: 48px,
),
color: (
+ icon-button-container-color: transparent,
icon-button-disabled-icon-color:
m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
+ icon-button-filled-container-color: map.get($system, primary),
+ icon-button-filled-disabled-container-color:
+ m3-utils.color-with-opacity(map.get($system, on-surface), 12%),
+ icon-button-filled-disabled-icon-color:
+ m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
+ icon-button-filled-disabled-state-layer-color: map.get($system, on-surface-variant),
+ icon-button-filled-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
+ icon-button-filled-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
+ icon-button-filled-icon-color: map.get($system, on-primary),
+ icon-button-filled-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
+ icon-button-filled-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)),
+ icon-button-filled-state-layer-color: map.get($system, on-primary),
+ icon-button-tonal-container-color: map.get($system, secondary-container),
+ icon-button-tonal-disabled-container-color:
+ m3-utils.color-with-opacity(map.get($system, on-surface), 12%),
+ icon-button-tonal-disabled-icon-color:
+ m3-utils.color-with-opacity(map.get($system, on-surface), 38%),
+ icon-button-tonal-disabled-state-layer-color: map.get($system, on-surface-variant),
+ icon-button-tonal-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
+ icon-button-tonal-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
+ icon-button-tonal-icon-color: map.get($system, on-secondary-container),
+ icon-button-tonal-pressed-state-layer-opacity: map.get($system, pressed-state-layer-opacity),
+ icon-button-tonal-ripple-color: m3-utils.color-with-opacity(
+ map.get($system, on-secondary-container), map.get($system, pressed-state-layer-opacity)),
+ icon-button-tonal-state-layer-color: map.get($system, on-secondary-container),
icon-button-disabled-state-layer-color: map.get($system, on-surface-variant),
icon-button-focus-state-layer-opacity: map.get($system, focus-state-layer-opacity),
icon-button-hover-state-layer-opacity: map.get($system, hover-state-layer-opacity),
diff --git a/src/material/button/button.md b/src/material/button/button.md
index bd7b8f4238ee..29bdbb772339 100644
--- a/src/material/button/button.md
+++ b/src/material/button/button.md
@@ -29,6 +29,9 @@ attribute, for example `matButton="outlined"`:
| `outlined` | Medium-emphasis buttons often used for actions that need attention but aren't the primary action. |
| `elevated` | Medium-emphasis buttons often used when a button requires visual separation from a patterned background. |
+The `matIconButton` supports filled and tonal appearances, for example
+`matIconButton="filled"` and `matIconButton="tonal"`.
+
### Extended FAB buttons
Traditional floating action buttons (FAB) buttons are circular and only have space for a single
diff --git a/src/material/button/button.spec.ts b/src/material/button/button.spec.ts
index 7c4679ee8334..c5f3377a9ef1 100644
--- a/src/material/button/button.spec.ts
+++ b/src/material/button/button.spec.ts
@@ -10,6 +10,7 @@ import {
MatButtonConfig,
MatButtonModule,
MatFabDefaultOptions,
+ MatIconButtonAppearance,
} from './index';
describe('MatButton', () => {
@@ -119,6 +120,49 @@ describe('MatButton', () => {
expect(button.classList).toContain('mat-mdc-outlined-button');
});
+ it('should apply the icon button appearance classes', () => {
+ const fixture = TestBed.createComponent(TestApp);
+ fixture.detectChanges();
+
+ const defaultIconButton = fixture.nativeElement.querySelector('.default-icon-button');
+ const filledIconButton = fixture.nativeElement.querySelector('.filled-icon-button');
+ const legacyIconButton = fixture.nativeElement.querySelector('.legacy-icon-button');
+ const tonalIconAnchor = fixture.nativeElement.querySelector('.tonal-icon-anchor');
+
+ expect(defaultIconButton.classList).not.toContain('mat-mdc-icon-button-filled');
+ expect(defaultIconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
+ expect(filledIconButton.classList).toContain('mat-mdc-icon-button-filled');
+ expect(legacyIconButton.classList).toContain('mat-mdc-icon-button-filled');
+ expect(tonalIconAnchor.classList).toContain('mat-mdc-icon-button-tonal');
+ });
+
+ it('should be able to change the icon button appearance dynamically', () => {
+ const fixture = TestBed.createComponent(TestApp);
+ const iconButton = fixture.nativeElement.querySelector('.dynamic-icon-button') as HTMLElement;
+ fixture.detectChanges();
+
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
+
+ fixture.componentInstance.iconButtonAppearance = 'filled';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ expect(iconButton.classList).toContain('mat-mdc-icon-button-filled');
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
+
+ fixture.componentInstance.iconButtonAppearance = 'tonal';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
+ expect(iconButton.classList).toContain('mat-mdc-icon-button-tonal');
+
+ fixture.componentInstance.iconButtonAppearance = '';
+ fixture.changeDetectorRef.markForCheck();
+ fixture.detectChanges();
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-filled');
+ expect(iconButton.classList).not.toContain('mat-mdc-icon-button-tonal');
+ });
+
describe('button[mat-fab]', () => {
it('should have accent palette by default', () => {
const fixture = TestBed.createComponent(TestApp);
@@ -540,6 +584,21 @@ describe('MatFabDefaultOptions', () => {
+
+
+
+
+ Tonal icon anchor
+
+
`,
imports: [MatButtonModule],
changeDetection: ChangeDetectionStrategy.Eager,
@@ -553,6 +612,7 @@ class TestApp {
extended = false;
disabledInteractive = false;
appearance: MatButtonAppearance = 'text';
+ iconButtonAppearance: MatIconButtonAppearance | '' = '';
showProgress = false;
increment() {
diff --git a/src/material/button/icon-button.scss b/src/material/button/icon-button.scss
index 11eb5b947036..b9497531647d 100644
--- a/src/material/button/icon-button.scss
+++ b/src/material/button/icon-button.scss
@@ -13,7 +13,8 @@ $fallbacks: m3-icon-button.get-tokens();
box-sizing: border-box;
border: none;
outline: none;
- background-color: transparent;
+ background-color: token-utils.slot(
+ icon-button-container-color, $fallbacks, $fallback: transparent);
fill: currentColor;
text-decoration: none;
cursor: pointer;
@@ -59,7 +60,38 @@ $fallbacks: m3-icon-button.get-tokens();
@include button-base.mat-private-button-disabled {
color: token-utils.slot(icon-button-disabled-icon-color, $fallbacks);
}
-;
+
+ &.mat-mdc-icon-button-filled {
+ background-color: token-utils.slot(icon-button-filled-container-color, $fallbacks);
+ color: token-utils.slot(icon-button-filled-icon-color, $fallbacks);
+
+ @include button-base.mat-private-button-ripple(
+ icon-button-filled-ripple-color, icon-button-filled-state-layer-color,
+ icon-button-filled-disabled-state-layer-color,
+ icon-button-filled-hover-state-layer-opacity, icon-button-filled-focus-state-layer-opacity,
+ icon-button-filled-pressed-state-layer-opacity, $fallbacks);
+
+ @include button-base.mat-private-button-disabled {
+ background-color: token-utils.slot(icon-button-filled-disabled-container-color, $fallbacks);
+ color: token-utils.slot(icon-button-filled-disabled-icon-color, $fallbacks);
+ }
+ }
+
+ &.mat-mdc-icon-button-tonal {
+ background-color: token-utils.slot(icon-button-tonal-container-color, $fallbacks);
+ color: token-utils.slot(icon-button-tonal-icon-color, $fallbacks);
+
+ @include button-base.mat-private-button-ripple(
+ icon-button-tonal-ripple-color, icon-button-tonal-state-layer-color,
+ icon-button-tonal-disabled-state-layer-color,
+ icon-button-tonal-hover-state-layer-opacity, icon-button-tonal-focus-state-layer-opacity,
+ icon-button-tonal-pressed-state-layer-opacity, $fallbacks);
+
+ @include button-base.mat-private-button-disabled {
+ background-color: token-utils.slot(icon-button-tonal-disabled-container-color, $fallbacks);
+ color: token-utils.slot(icon-button-tonal-disabled-icon-color, $fallbacks);
+ }
+ }
img,
svg {
diff --git a/src/material/button/icon-button.ts b/src/material/button/icon-button.ts
index 1582882d5b36..9a38ead0b148 100644
--- a/src/material/button/icon-button.ts
+++ b/src/material/button/icon-button.ts
@@ -6,9 +6,18 @@
* found in the LICENSE file at https://angular.dev/license
*/
-import {Component, ViewEncapsulation} from '@angular/core';
+import {Component, Input, ViewEncapsulation} from '@angular/core';
import {MatButtonBase} from './button-base';
+/** Possible appearances for a `MatIconButton`. */
+export type MatIconButtonAppearance = 'filled' | 'tonal';
+
+/** Classes that need to be set for each appearance of the icon button. */
+const APPEARANCE_CLASSES: Map = new Map([
+ ['filled', ['mat-mdc-icon-button-filled']],
+ ['tonal', ['mat-mdc-icon-button-tonal']],
+]);
+
/**
* Material Design icon button component. This type of button displays a single interactive icon for
* users to perform an action.
@@ -25,10 +34,51 @@ import {MatButtonBase} from './button-base';
encapsulation: ViewEncapsulation.None,
})
export class MatIconButton extends MatButtonBase {
+ /** Appearance of the icon button. */
+ @Input('matIconButton')
+ get appearance(): MatIconButtonAppearance | null {
+ return this._appearance;
+ }
+ set appearance(value: MatIconButtonAppearance | '') {
+ this.setAppearance(value || null);
+ }
+ private _appearance: MatIconButtonAppearance | null = null;
+
+ /** Same as `appearance`, but using the legacy `mat-icon-button` attribute selector. */
+ @Input('mat-icon-button')
+ set _legacyAppearance(value: MatIconButtonAppearance | '') {
+ this.setAppearance(value || null);
+ }
+
constructor() {
super();
this._rippleLoader.configureRipple(this._elementRef.nativeElement, {centered: true});
}
+
+ /** Programmatically sets the appearance of the icon button. */
+ setAppearance(appearance: MatIconButtonAppearance | null): void {
+ if (appearance === this._appearance) {
+ return;
+ }
+
+ const classList = this._elementRef.nativeElement.classList;
+ const previousClasses = this._appearance ? APPEARANCE_CLASSES.get(this._appearance) : null;
+ const newClasses = appearance ? APPEARANCE_CLASSES.get(appearance) : null;
+
+ if ((typeof ngDevMode === 'undefined' || ngDevMode) && appearance && !newClasses) {
+ throw new Error(`Unsupported MatIconButton appearance "${appearance}"`);
+ }
+
+ if (previousClasses) {
+ classList.remove(...previousClasses);
+ }
+
+ if (newClasses) {
+ classList.add(...newClasses);
+ }
+
+ this._appearance = appearance;
+ }
}
// tslint:disable:variable-name
diff --git a/src/material/button/testing/button-harness.spec.ts b/src/material/button/testing/button-harness.spec.ts
index 85ef3b581846..8792cedaa1a1 100644
--- a/src/material/button/testing/button-harness.spec.ts
+++ b/src/material/button/testing/button-harness.spec.ts
@@ -21,7 +21,7 @@ describe('MatButtonHarness', () => {
it('should load all button harnesses', async () => {
const buttons = await loader.getAllHarnesses(MatButtonHarness);
- expect(buttons.length).toBe(22);
+ expect(buttons.length).toBe(24);
});
it('should load button with exact text', async () => {
@@ -40,7 +40,7 @@ describe('MatButtonHarness', () => {
it('should filter by whether a button is disabled', async () => {
const enabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: false}));
const disabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: true}));
- expect(enabledButtons.length).toBe(20);
+ expect(enabledButtons.length).toBe(22);
expect(disabledButtons.length).toBe(2);
});
@@ -134,6 +134,8 @@ describe('MatButtonHarness', () => {
'basic',
'icon',
'icon',
+ 'icon',
+ 'icon',
'fab',
'mini-fab',
'basic',
@@ -164,6 +166,8 @@ describe('MatButtonHarness', () => {
'tonal',
null,
null,
+ 'filled',
+ 'tonal',
null,
null,
'text',
@@ -188,8 +192,10 @@ describe('MatButtonHarness', () => {
});
it('should be able to filter buttons based on their appearance', async () => {
- const button = await loader.getHarness(MatButtonHarness.with({appearance: 'filled'}));
- expect(await button.getText()).toBe('Filled button');
+ const buttons = await loader.getAllHarnesses(MatButtonHarness.with({appearance: 'filled'}));
+ const texts = await parallel(() => buttons.map(button => button.getText()));
+
+ expect(texts).toEqual(['Filled button', 'add', 'Filled anchor']);
});
it('should get the appearance of a button with a dynamic appearance', async () => {
@@ -243,6 +249,12 @@ describe('MatButtonHarness', () => {
+
+
diff --git a/src/material/button/testing/button-harness.ts b/src/material/button/testing/button-harness.ts
index 06184c91e5f1..237d2aa0f580 100644
--- a/src/material/button/testing/button-harness.ts
+++ b/src/material/button/testing/button-harness.ts
@@ -148,6 +148,14 @@ export class MatButtonHarness extends ContentContainerComponentHarness {
async getAppearance(): Promise {
const host = await this.host();
+ if (await host.hasClass('mat-mdc-icon-button-filled')) {
+ return 'filled';
+ }
+
+ if (await host.hasClass('mat-mdc-icon-button-tonal')) {
+ return 'tonal';
+ }
+
if (await host.hasClass('mat-mdc-outlined-button')) {
return 'outlined';
}