Skip to content

Commit fb81ab4

Browse files
authoredMar 18, 2025··
feat(material/button): add support for tonal button (#30638)
Adds support for the tonal button appearance from the spec. It can be enabled by setting `matButton="tonal"` on the button. Fixes #28809.
1 parent 443df26 commit fb81ab4

22 files changed

+379
-72
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
section {
2-
display: table;
2+
display: flex;
3+
align-items: center;
34
}
45

56
.example-label {
6-
display: table-cell;
77
font-size: 14px;
8-
margin-left: 8px;
8+
margin: 0 16px 0 8px;
99
min-width: 120px;
1010
}
1111

1212
.example-button-row {
13-
display: table-cell;
1413
max-width: 600px;
1514
}
1615

@@ -23,9 +22,3 @@ section {
2322
justify-content: space-between;
2423
flex-wrap: wrap;
2524
}
26-
27-
.example-button-container {
28-
display: flex;
29-
justify-content: center;
30-
width: 140px;
31-
}

‎src/components-examples/material/button/button-overview/button-overview-example.html

+42-46
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<a matButton href="https://www.google.com/" target="_blank">Link</a>
77
</div>
88
</section>
9-
<mat-divider></mat-divider>
9+
<mat-divider/>
1010
<section>
1111
<div class="example-label">Elevated</div>
1212
<div class="example-button-row">
@@ -15,7 +15,7 @@
1515
<a matButton="elevated" href="https://www.google.com/" target="_blank">Link</a>
1616
</div>
1717
</section>
18-
<mat-divider></mat-divider>
18+
<mat-divider/>
1919
<section>
2020
<div class="example-label">Outlined</div>
2121
<div class="example-button-row">
@@ -24,16 +24,25 @@
2424
<a matButton="outlined" href="https://www.google.com/" target="_blank">Link</a>
2525
</div>
2626
</section>
27-
<mat-divider></mat-divider>
27+
<mat-divider/>
2828
<section>
2929
<div class="example-label">Filled</div>
3030
<div class="example-button-row">
31-
<button matButton="filled" >Basic</button>
32-
<button matButton="filled" disabled>Disabled</button>
31+
<button matButton="filled">Basic</button>
32+
<button matButton="filled" disabled>Disabled</button>
3333
<a matButton="filled" href="https://www.google.com/" target="_blank">Link</a>
3434
</div>
3535
</section>
36-
<mat-divider></mat-divider>
36+
<mat-divider/>
37+
<section>
38+
<div class="example-label">Tonal</div>
39+
<div class="example-button-row">
40+
<button matButton="tonal" >Basic</button>
41+
<button matButton="tonal" disabled>Disabled</button>
42+
<a matButton="tonal" href="https://www.google.com/" target="_blank">Link</a>
43+
</div>
44+
</section>
45+
<mat-divider/>
3746
<section>
3847
<div class="example-label">Icon</div>
3948
<div class="example-button-row">
@@ -47,64 +56,51 @@
4756
</div>
4857
</div>
4958
</section>
50-
<mat-divider></mat-divider>
59+
<mat-divider/>
5160
<section>
5261
<div class="example-label">Floating Action Button (FAB)</div>
5362
<div class="example-button-row">
5463
<div class="example-flex-container">
55-
<div class="example-button-container">
56-
<button matFab aria-label="Example icon button with a delete icon">
57-
<mat-icon>delete</mat-icon>
58-
</button>
59-
</div>
60-
<div class="example-button-container">
61-
<button matFab disabled aria-label="Example icon button with a heart icon">
62-
<mat-icon>favorite</mat-icon>
63-
</button>
64-
</div>
64+
<button matFab aria-label="Example icon button with a delete icon">
65+
<mat-icon>delete</mat-icon>
66+
</button>
67+
<button matFab disabled aria-label="Example icon button with a heart icon">
68+
<mat-icon>favorite</mat-icon>
69+
</button>
6570
</div>
6671
</div>
6772
</section>
68-
<mat-divider></mat-divider>
73+
<mat-divider/>
6974
<section>
7075
<div class="example-label">Mini FAB</div>
7176
<div class="example-button-row">
7277
<div class="example-flex-container">
73-
<div class="example-button-container">
74-
<button matMiniFab aria-label="Example icon button with a menu icon">
75-
<mat-icon>menu</mat-icon>
76-
</button>
77-
</div>
78-
<div class="example-button-container">
79-
<button matMiniFab disabled aria-label="Example icon button with a home icon">
80-
<mat-icon>home</mat-icon>
81-
</button>
82-
</div>
78+
<button matMiniFab aria-label="Example icon button with a menu icon">
79+
<mat-icon>menu</mat-icon>
80+
</button>
81+
<button matMiniFab disabled aria-label="Example icon button with a home icon">
82+
<mat-icon>home</mat-icon>
83+
</button>
8384
</div>
8485
</div>
8586
</section>
87+
<mat-divider/>
8688
<section>
8789
<div class="example-label">Extended FAB</div>
8890
<div class="example-button-row">
8991
<div class="example-flex-container">
90-
<div class="example-button-container">
91-
<button matFab extended>
92-
<mat-icon>favorite</mat-icon>
93-
Basic
94-
</button>
95-
</div>
96-
<div class="example-button-container">
97-
<button matFab extended disabled>
98-
<mat-icon>favorite</mat-icon>
99-
Disabled
100-
</button>
101-
</div>
102-
<div class="example-button-container">
103-
<a matFab extended routerLink=".">
104-
<mat-icon>favorite</mat-icon>
105-
Link
106-
</a>
107-
</div>
92+
<button matFab extended>
93+
<mat-icon>favorite</mat-icon>
94+
Basic
95+
</button>
96+
<button matFab extended disabled>
97+
<mat-icon>favorite</mat-icon>
98+
Disabled
99+
</button>
100+
<a matFab extended routerLink=".">
101+
<mat-icon>favorite</mat-icon>
102+
Link
103+
</a>
108104
</div>
109105
</div>
110106
</section>

‎src/dev-app/button/button-demo.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ export class ButtonDemo {
4949
toggleDisable = false;
5050
tooltipText = 'This is a button tooltip!';
5151
disabledInteractive = false;
52-
appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled'];
52+
appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled', 'tonal'];
5353
}

‎src/material/button/_button-theme.scss

+52
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button;
1212
@use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button;
1313
@use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button;
14+
@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button;
1415
@use '../core/style/sass-utils';
1516

1617
@mixin _text-button-variant($theme, $palette) {
@@ -81,6 +82,15 @@
8182
@include token-utils.create-token-values(tokens-mat-outlined-button.$prefix, $mat-tokens);
8283
}
8384

85+
@mixin _tonal-button-variant($theme, $palette) {
86+
@include token-utils.create-token-values(tokens-mat-tonal-button.$prefix, if(
87+
$palette,
88+
tokens-mat-tonal-button.private-get-color-palette-color-tokens($theme, $palette),
89+
tokens-mat-tonal-button.get-color-tokens($theme)
90+
));
91+
}
92+
93+
8494
@mixin _theme-from-tokens($tokens, $options...) {
8595
@include validation.selector-defined(
8696
'Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector'
@@ -125,6 +135,11 @@
125135
tokens-mat-outlined-button.$prefix,
126136
$options...
127137
);
138+
$mat-tonal-button-tokens: token-utils.get-tokens-for(
139+
$tokens,
140+
tokens-mat-tonal-button.$prefix,
141+
$options...
142+
);
128143

129144
@include token-utils.create-token-values(tokens-mdc-text-button.$prefix, $mdc-text-button-tokens);
130145
@include token-utils.create-token-values(
@@ -152,6 +167,10 @@
152167
tokens-mat-outlined-button.$prefix,
153168
$mat-outlined-button-tokens
154169
);
170+
@include token-utils.create-token-values(
171+
tokens-mat-tonal-button.$prefix,
172+
$mat-tonal-button-tokens
173+
);
155174
}
156175

157176
/// Outputs base theme styles (styles not dependent on the color, typography, or density settings)
@@ -195,6 +214,10 @@
195214
tokens-mat-outlined-button.$prefix,
196215
tokens-mat-outlined-button.get-unthemable-tokens()
197216
);
217+
@include token-utils.create-token-values(
218+
tokens-mat-tonal-button.$prefix,
219+
tokens-mat-tonal-button.get-unthemable-tokens()
220+
);
198221
}
199222
}
200223
}
@@ -211,6 +234,7 @@
211234
@include sass-utils.current-selector-or-root() {
212235
@include _text-button-variant($theme, null);
213236
@include _filled-button-variant($theme, null);
237+
@include _tonal-button-variant($theme, null);
214238
@include _protected-button-variant($theme, null);
215239
@include _outlined-button-variant($theme, null);
216240
}
@@ -270,6 +294,20 @@
270294
@include _outlined-button-variant($theme, warn);
271295
}
272296
}
297+
298+
.mat-tonal-button {
299+
&.mat-primary {
300+
@include _tonal-button-variant($theme, primary);
301+
}
302+
303+
&.mat-accent {
304+
@include _tonal-button-variant($theme, accent);
305+
}
306+
307+
&.mat-warn {
308+
@include _tonal-button-variant($theme, warn);
309+
}
310+
}
273311
}
274312
}
275313

@@ -313,6 +351,10 @@
313351
tokens-mat-outlined-button.$prefix,
314352
tokens-mat-outlined-button.get-typography-tokens($theme)
315353
);
354+
@include token-utils.create-token-values(
355+
tokens-mat-tonal-button.$prefix,
356+
tokens-mat-tonal-button.get-typography-tokens($theme)
357+
);
316358
}
317359
}
318360
}
@@ -357,6 +399,10 @@
357399
tokens-mat-outlined-button.$prefix,
358400
tokens-mat-outlined-button.get-density-tokens($theme)
359401
);
402+
@include token-utils.create-token-values(
403+
tokens-mat-tonal-button.$prefix,
404+
tokens-mat-tonal-button.get-density-tokens($theme)
405+
);
360406
}
361407
}
362408
}
@@ -371,6 +417,7 @@
371417
$mat-protected-button-tokens: tokens-mat-protected-button.get-token-slots();
372418
$mdc-text-button-tokens: tokens-mdc-text-button.get-token-slots();
373419
$mat-text-button-tokens: tokens-mat-text-button.get-token-slots();
420+
$mat-tonal-button-tokens: tokens-mat-tonal-button.get-token-slots();
374421

375422
@return (
376423
(
@@ -413,6 +460,11 @@
413460
tokens: $mat-text-button-tokens,
414461
prefix: 'text-',
415462
),
463+
(
464+
namespace: tokens-mat-tonal-button.$prefix,
465+
tokens: $mat-tonal-button-tokens,
466+
prefix: 'tonal-',
467+
),
416468
);
417469
}
418470

‎src/material/button/button-base.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
2828
* Possible appearances for a `MatButton`.
2929
* See https://m3.material.io/components/buttons/overview
3030
*/
31-
export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
31+
export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
3232

3333
/** Object that can be used to configure the default options for the button component. */
3434
export interface MatButtonConfig {

‎src/material/button/button-high-contrast.scss

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.mat-mdc-unelevated-button:not(.mdc-button--outlined),
55
.mat-mdc-raised-button:not(.mdc-button--outlined),
66
.mat-mdc-outlined-button:not(.mdc-button--outlined),
7+
.mat-mdc-button-base.mat-tonal-button,
78
.mat-mdc-icon-button.mat-mdc-icon-button,
89
.mat-mdc-outlined-button .mdc-button__ripple {
910
@include cdk.high-contrast {

‎src/material/button/button.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ There are several button variants, each applied as an attribute:
2020
Additionally, the `matButton` has several appearances that can be set using the `matButton`
2121
attribute, for example `matButton="outlined"`:
2222

23-
2423
| Appearance | Description |
2524
|--------------|----------------------------------------------------------------------------------|
26-
| `text` | Default appearance. Does not have a background until the user interacts with it. |
27-
| `elevated` | Has a background color, elevation and rounded corners. |
28-
| `filled` | Has a flat appearance with rounded corners and no elevation. |
29-
| `outlined` | Has an outline, rounded corners and a transparent background. |
25+
| `text` | Default appearance. Text buttons are used for the lowest priority actions, especially when presenting multiple options. |
26+
| `filled` | High-emphasis buttons used for final or unblocking actions in a flow, such as saving or confirming. |
27+
| `tonal` | Medium-emphasis buttons often used for final or unblocking actions in a flow, but with less visual emphasis than a filled button. |
28+
| `outlined` | Medium-emphasis buttons often used for actions that need attention but aren't the primary action. |
29+
| `elevated` | Medium-emphasis buttons often used when a button requires visual separation from a patterned background. |
3030

3131

3232
### Extended FAB buttons

‎src/material/button/button.scss

+41-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
@use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button;
1212
@use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button;
1313
@use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button;
14+
@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button;
1415

1516
.mat-mdc-button-base {
1617
text-decoration: none;
@@ -267,10 +268,48 @@
267268
}
268269
}
269270

271+
.mat-tonal-button {
272+
$slots: tokens-mat-tonal-button.get-token-slots();
273+
transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
274+
275+
@include token-utils.use-tokens(tokens-mat-tonal-button.$prefix, $slots) {
276+
@include token-utils.create-token-slot(height, container-height);
277+
@include token-utils.create-token-slot(font-family, label-text-font);
278+
@include token-utils.create-token-slot(font-size, label-text-size);
279+
@include token-utils.create-token-slot(letter-spacing, label-text-tracking);
280+
@include token-utils.create-token-slot(text-transform, label-text-transform);
281+
@include token-utils.create-token-slot(font-weight, label-text-weight);
282+
padding: 0 #{token-utils.get-token-variable(horizontal-padding, true)};
283+
284+
&:not(:disabled) {
285+
@include token-utils.create-token-slot(color, label-text-color);
286+
@include token-utils.create-token-slot(background-color, container-color);
287+
}
288+
289+
&, .mdc-button__ripple {
290+
@include token-utils.create-token-slot(border-radius, container-shape);
291+
}
292+
293+
// We need to re-apply the disabled tokens since MDC uses
294+
// `:disabled` which doesn't apply to anchors.
295+
@include button-base.mat-private-button-disabled {
296+
@include token-utils.create-token-slot(color, disabled-label-text-color);
297+
@include token-utils.create-token-slot(background-color, disabled-container-color);
298+
}
299+
}
300+
301+
@include button-base.mat-private-button-horizontal-layout(tokens-mat-tonal-button.$prefix,
302+
$slots, false);
303+
@include button-base.mat-private-button-ripple(tokens-mat-tonal-button.$prefix, $slots);
304+
@include button-base.mat-private-button-touch-target(false, tokens-mat-tonal-button.$prefix,
305+
$slots);
306+
}
307+
270308
.mat-mdc-button,
271309
.mat-mdc-unelevated-button,
272310
.mat-mdc-raised-button,
273-
.mat-mdc-outlined-button {
311+
.mat-mdc-outlined-button,
312+
.mat-tonal-button {
274313
@include button-base.mat-private-button-interactive();
275314
@include style-private.private-animation-noop();
276315

@@ -306,6 +345,7 @@
306345
// For the button element, default inset/offset values are necessary to ensure that
307346
// the focus indicator is sufficiently contrastive and renders appropriately.
308347
.mat-mdc-unelevated-button,
348+
.mat-tonal-button,
309349
.mat-mdc-raised-button {
310350
.mat-focus-indicator::before {
311351
$default-border-width: focus-indicators-private.$default-border-width;

‎src/material/button/button.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const APPEARANCE_CLASSES: Map<MatButtonAppearance, readonly string[]> = new Map(
1818
['filled', ['mdc-button--unelevated', 'mat-mdc-unelevated-button']],
1919
['elevated', ['mdc-button--raised', 'mat-mdc-raised-button']],
2020
['outlined', ['mdc-button--outlined', 'mat-mdc-outlined-button']],
21+
['tonal', ['mat-tonal-button']],
2122
]);
2223

2324
/**

‎src/material/button/testing/button-harness-filters.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {BaseHarnessFilters} from '@angular/cdk/testing';
1212
export type ButtonVariant = 'basic' | 'icon' | 'fab' | 'mini-fab';
1313

1414
/** Possible button appearances. */
15-
export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
15+
export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
1616

1717
/** A set of criteria that can be used to filter a list of button harness instances. */
1818
export interface ButtonHarnessFilters extends BaseHarnessFilters {

‎src/material/button/testing/button-harness.spec.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('MatButtonHarness', () => {
2222

2323
it('should load all button harnesses', async () => {
2424
const buttons = await loader.getAllHarnesses(MatButtonHarness);
25-
expect(buttons.length).toBe(15);
25+
expect(buttons.length).toBe(16);
2626
});
2727

2828
it('should load button with exact text', async () => {
@@ -41,7 +41,7 @@ describe('MatButtonHarness', () => {
4141
it('should filter by whether a button is disabled', async () => {
4242
const enabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: false}));
4343
const disabledButtons = await loader.getAllHarnesses(MatButtonHarness.with({disabled: true}));
44-
expect(enabledButtons.length).toBe(13);
44+
expect(enabledButtons.length).toBe(14);
4545
expect(disabledButtons.length).toBe(2);
4646
});
4747

@@ -118,6 +118,7 @@ describe('MatButtonHarness', () => {
118118
'basic',
119119
'basic',
120120
'basic',
121+
'basic',
121122
'icon',
122123
'icon',
123124
'fab',
@@ -141,6 +142,7 @@ describe('MatButtonHarness', () => {
141142
'filled',
142143
'elevated',
143144
'outlined',
145+
'tonal',
144146
null,
145147
null,
146148
null,
@@ -178,6 +180,7 @@ describe('MatButtonHarness', () => {
178180
</button>
179181
<button id="raised" type="button" matButton="elevated">Elevated button</button>
180182
<button id="stroked" type="button" matButton="outlined">Outlined button</button>
183+
<button id="tonal" type="button" matButton="tonal">Tonal button</button>
181184
<button id="home-icon" type="button" matIconButton>
182185
<mat-icon>home</mat-icon>
183186
</button>

‎src/material/button/testing/button-harness.ts

+4
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export class MatButtonHarness extends ContentContainerComponentHarness {
140140
return 'text';
141141
}
142142

143+
if (await host.hasClass('mat-tonal-button')) {
144+
return 'tonal';
145+
}
146+
143147
return null;
144148
}
145149
}

‎src/material/core/tokens/_density.scss

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ $_density-tokens: (
3838
(mdc, filled-button): (
3939
container-height: (40px, 36px, 32px, 28px),
4040
),
41+
(mat, tonal-button): (
42+
container-height: (40px, 36px, 32px, 28px),
43+
),
4144
(mdc, outlined-button): (
4245
container-height: (40px, 36px, 32px, 28px),
4346
),

‎src/material/core/tokens/_token-definition.scss

+1-2
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,7 @@ $_system-fallbacks: null;
173173
@if(meta.type-of($color) == 'color') {
174174
$result: map.remove($result, $opacity-key);
175175
$result: map.set($result, $color-key, rgba($color, $opacity));
176-
}
177-
@else if($color != null) {
176+
} @else if($color != null) {
178177
$result: map.remove($result, $opacity-key);
179178
$combined-color: #{color-mix(in srgb, #{$color} #{($opacity * 100) + '%'}, transparent)};
180179
$result: map.set($result, $color-key, $combined-color);

‎src/material/core/tokens/m2/_index.scss

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
@use './mat/toolbar' as tokens-mat-toolbar;
4646
@use './mat/tree' as tokens-mat-tree;
4747
@use './mat/timepicker' as tokens-mat-timepicker;
48+
@use './mat/tonal-button' as tokens-mat-tonal-button;
4849
@use './mdc/checkbox' as tokens-mdc-checkbox;
4950
@use './mdc/text-button' as tokens-mdc-text-button;
5051
@use './mdc/protected-button' as tokens-mdc-protected-button;
@@ -158,6 +159,7 @@
158159
_get-tokens-for-module($theme, tokens-mat-toolbar),
159160
_get-tokens-for-module($theme, tokens-mat-tree),
160161
_get-tokens-for-module($theme, tokens-mat-timepicker),
162+
_get-tokens-for-module($theme, tokens-mat-tonal-button),
161163
_get-tokens-for-module($theme, tokens-mdc-checkbox),
162164
_get-tokens-for-module($theme, tokens-mdc-chip),
163165
_get-tokens-for-module($theme, tokens-mdc-circular-progress),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
@use 'sass:map';
2+
@use '../../token-definition';
3+
@use '../../../theming/theming';
4+
@use '../../../theming/inspection';
5+
@use '../../../style/sass-utils';
6+
7+
// The prefix used to generate the fully qualified name for tokens in this file.
8+
$prefix: (mat, tonal-button);
9+
10+
// Tokens that can't be configured through Angular Material's current theming API,
11+
// but may be in a future version of the theming API.
12+
@function get-unthemable-tokens() {
13+
@return (
14+
// Shape of the container element.
15+
container-shape: 4px,
16+
17+
// Start/end padding of the button.
18+
horizontal-padding: 16px,
19+
20+
// Space between the icon and the button's main content.
21+
icon-spacing: 8px,
22+
23+
// Amount by which to offset the icon so that its presence
24+
// doesn't increase throw off the horizontal padding.
25+
icon-offset: -4px,
26+
);
27+
}
28+
29+
// Tokens that can be configured through Angular Material's color theming API.
30+
@function get-color-tokens($theme) {
31+
$is-dark: inspection.get-theme-type($theme) == dark;
32+
33+
@return (
34+
container-color: inspection.get-theme-color($theme, background, card),
35+
label-text-color: inspection.get-theme-color($theme, foreground, text, 1),
36+
disabled-container-color: inspection.get-theme-color($theme, foreground, disabled-button,
37+
0.12),
38+
disabled-label-text-color: inspection.get-theme-color($theme, foreground, disabled-button,
39+
if($is-dark, 0.5, 0.38)),
40+
41+
// Color of the element that shows the hover, focus and pressed states.
42+
state-layer-color: inspection.get-theme-color($theme, foreground, base),
43+
44+
// Color of the element that shows the hover, focus and pressed states while disabled.
45+
disabled-state-layer-color: inspection.get-theme-color($theme, foreground, base),
46+
47+
// Color of the ripple element.
48+
ripple-color: inspection.get-theme-color($theme, foreground, base, 0.1),
49+
50+
// Opacity of the ripple when the button is hovered.
51+
hover-state-layer-opacity: if($is-dark, 0.08, 0.04),
52+
53+
// Opacity of the ripple when the button is focused.
54+
focus-state-layer-opacity: if($is-dark, 0.24, 0.12),
55+
56+
// Opacity of the ripple when the button is pressed.
57+
pressed-state-layer-opacity: if($is-dark, 0.24, 0.12),
58+
);
59+
}
60+
61+
// Generates the mapping for the properties that change based on the button palette color.
62+
@function private-get-color-palette-color-tokens($theme, $palette-name) {
63+
@return (
64+
container-color: inspection.get-theme-color($theme, $palette-name, default),
65+
label-text-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1),
66+
state-layer-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1),
67+
ripple-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 0.1),
68+
);
69+
}
70+
71+
// Tokens that can be configured through Angular Material's typography theming API.
72+
@function get-typography-tokens($theme) {
73+
@return (
74+
label-text-font: inspection.get-theme-typography($theme, button, font-family),
75+
label-text-size: inspection.get-theme-typography($theme, button, font-size),
76+
label-text-tracking: inspection.get-theme-typography($theme, button, letter-spacing),
77+
label-text-weight: inspection.get-theme-typography($theme, button, font-weight),
78+
label-text-transform: none,
79+
);
80+
}
81+
82+
// Tokens that can be configured through Angular Material's density theming API.
83+
@function get-density-tokens($theme) {
84+
$scale: theming.clamp-density(inspection.get-theme-density($theme), -3);
85+
86+
@return (
87+
touch-target-display: if($scale < -1, none, block),
88+
container-height:
89+
map.get(
90+
(
91+
0: 36px,
92+
-1: 32px,
93+
-2: 28px,
94+
-3: 24px,
95+
),
96+
$scale
97+
)
98+
);
99+
}
100+
101+
// Combines the tokens generated by the above functions into a single map with placeholder values.
102+
// This is used to create token slots.
103+
@function get-token-slots() {
104+
@return sass-utils.deep-merge-all(
105+
get-unthemable-tokens(),
106+
get-color-tokens(token-definition.$placeholder-color-config),
107+
get-typography-tokens(token-definition.$placeholder-typography-config),
108+
get-density-tokens(token-definition.$placeholder-density-config)
109+
);
110+
}

‎src/material/core/tokens/m3/_index.scss

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
@use './mat/toolbar' as tokens-mat-toolbar;
4444
@use './mat/tree' as tokens-mat-tree;
4545
@use './mat/timepicker' as tokens-mat-timepicker;
46+
@use './mat/tonal-button' as tokens-mat-tonal-button;
4647
@use './mdc/checkbox' as tokens-mdc-checkbox;
4748
@use './mdc/text-button' as tokens-mdc-text-button;
4849
@use './mdc/protected-button' as tokens-mdc-protected-button;
@@ -85,6 +86,7 @@ $_module-names: (
8586
tokens-mat-fab,
8687
tokens-mat-fab-small,
8788
tokens-mat-filled-button,
89+
tokens-mat-tonal-button,
8890
tokens-mat-form-field,
8991
tokens-mat-grid-list,
9092
tokens-mat-icon-button,

‎src/material/core/tokens/m3/definitions/_index.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
@forward './md-comp-fab-tertiary-small' as md-comp-fab-tertiary-small-*;
1515
@forward './md-comp-fab-tertiary' as md-comp-fab-tertiary-*;
1616
@forward './md-comp-filled-button' as md-comp-filled-button-*;
17+
@forward './md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*;
1718
@forward './md-comp-filled-card' as md-comp-filled-card-*;
1819
@forward './md-comp-filled-icon-button' as md-comp-filled-icon-button-*;
1920
@forward './md-comp-filled-text-field' as md-comp-filled-text-field-*;
@@ -63,7 +64,6 @@
6364
// @forward './unused/md-comp-filled-autocomplete' as md-comp-filled-autocomplete-*;
6465
// @forward './unused/md-comp-filled-menu-button' as md-comp-filled-menu-button-*;
6566
// @forward './unused/md-comp-filled-select' as md-comp-filled-select-*;
66-
// @forward './unused/md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*;
6767
// @forward './unused/md-comp-filled-tonal-icon-button' as md-comp-filled-tonal-icon-button-*;
6868
// @forward './unused/md-comp-filter-chip' as md-comp-filter-chip-*;
6969
// @forward './unused/md-comp-full-screen-dialog' as md-comp-full-screen-dialog-*;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
@use 'sass:map';
2+
@use 'sass:meta';
3+
@use '../../../style/sass-utils';
4+
@use '../../token-definition';
5+
6+
// The prefix used to generate the fully qualified name for tokens in this file.
7+
$prefix: (mat, tonal-button);
8+
9+
/// Generates custom tokens for the mat-flat-button.
10+
/// @param {Map} $systems The MDC system tokens
11+
/// @param {Boolean} $exclude-hardcoded Whether to exclude hardcoded token values
12+
/// @param {Map} $token-slots Possible token slots
13+
/// @return {Map} A set of custom tokens for the mat-flat-button
14+
@function get-tokens($systems, $exclude-hardcoded, $token-slots) {
15+
$mdc-tokens: token-definition.get-mdc-tokens('filled-tonal-button', $systems, $exclude-hardcoded);
16+
17+
$tokens: map.merge($mdc-tokens, (
18+
horizontal-padding: token-definition.hardcode(24px, $exclude-hardcoded),
19+
icon-spacing: token-definition.hardcode(8px, $exclude-hardcoded),
20+
icon-offset: token-definition.hardcode(-8px, $exclude-hardcoded),
21+
state-layer-color: map.get($systems, md-sys-color, on-secondary-container),
22+
disabled-state-layer-color: map.get($systems, md-sys-color, on-surface-variant),
23+
ripple-color: sass-utils.safe-color-change(
24+
map.get($systems, md-sys-color, on-secondary-container),
25+
$alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
26+
),
27+
hover-state-layer-opacity: map.get($systems, md-sys-state, hover-state-layer-opacity),
28+
focus-state-layer-opacity: map.get($systems, md-sys-state, focus-state-layer-opacity),
29+
pressed-state-layer-opacity: map.get($systems, md-sys-state, pressed-state-layer-opacity),
30+
));
31+
32+
$variant-tokens: (
33+
// Color variants:
34+
primary: (),
35+
secondary: (),
36+
tertiary: (
37+
container-color: map.get($systems, md-sys-color, tertiary-container),
38+
focus-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
39+
focus-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
40+
hover-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
41+
hover-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
42+
label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
43+
pressed-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
44+
pressed-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
45+
with-icon-focus-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
46+
with-icon-hover-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
47+
with-icon-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
48+
with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
49+
state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
50+
ripple-color: sass-utils.safe-color-change(
51+
map.get($systems, md-sys-color, on-tertiary-container),
52+
$alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
53+
),
54+
),
55+
error: (
56+
container-color: map.get($systems, md-sys-color, error-container),
57+
focus-label-text-color: map.get($systems, md-sys-color, on-error-container),
58+
focus-state-layer-color: map.get($systems, md-sys-color, on-error-container),
59+
hover-label-text-color: map.get($systems, md-sys-color, on-error-container),
60+
hover-state-layer-color: map.get($systems, md-sys-color, on-error-container),
61+
label-text-color: map.get($systems, md-sys-color, on-error-container),
62+
pressed-label-text-color: map.get($systems, md-sys-color, on-error-container),
63+
pressed-state-layer-color: map.get($systems, md-sys-color, on-error-container),
64+
with-icon-focus-icon-color: map.get($systems, md-sys-color, on-error-container),
65+
with-icon-hover-icon-color: map.get($systems, md-sys-color, on-error-container),
66+
with-icon-icon-color: map.get($systems, md-sys-color, on-error-container),
67+
with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-error-container),
68+
state-layer-color: map.get($systems, md-sys-color, on-error-container),
69+
ripple-color: sass-utils.safe-color-change(
70+
map.get($systems, md-sys-color, on-error-container),
71+
$alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
72+
),
73+
)
74+
);
75+
76+
@return token-definition.namespace-tokens($prefix, (
77+
_fix-tokens($tokens),
78+
token-definition.map-values($variant-tokens, meta.get-function(_fix-tokens))
79+
), $token-slots);
80+
}
81+
82+
83+
/// Fixes inconsistent values in the tonal button tokens so that they can produce valid styles.
84+
/// @param {Map} $initial-tokens Map of tonal button tokens currently being generated.
85+
/// @return {Map} The given tokens, with the invalid values replaced with valid ones.
86+
@function _fix-tokens($initial-tokens) {
87+
// Need to get the hardcoded values, because they include opacities that are used for the disabled
88+
// state.
89+
$hardcoded-tokens: token-definition.get-mdc-tokens('filled-tonal-button', (), false);
90+
91+
@return token-definition.combine-color-tokens($initial-tokens, $hardcoded-tokens, (
92+
(
93+
color: disabled-label-text-color,
94+
opacity: disabled-label-text-opacity,
95+
),
96+
(
97+
color: disabled-container-color,
98+
opacity: disabled-container-opacity,
99+
)
100+
));
101+
}

‎tools/public_api_guard/material/button-testing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ContentContainerComponentHarness } from '@angular/cdk/testing';
1010
import { HarnessPredicate } from '@angular/cdk/testing';
1111

1212
// @public
13-
export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
13+
export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
1414

1515
// @public
1616
export interface ButtonHarnessFilters extends BaseHarnessFilters {

‎tools/public_api_guard/material/button.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class MatButton extends MatButtonBase {
4444
}
4545

4646
// @public
47-
export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
47+
export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
4848

4949
// @public
5050
export interface MatButtonConfig {

0 commit comments

Comments
 (0)
Please sign in to comment.