From 5693a07b924910157b5408fa181a6dcb4ff40d1d Mon Sep 17 00:00:00 2001
From: Kristiyan Kostadinov <crisbeto@abv.bg>
Date: Tue, 18 Mar 2025 08:41:38 +0100
Subject: [PATCH] feat(material/button): add support for tonal button

Adds support for the tonal button appearance from the spec. It can be enabled by setting `matButton="tonal"` on the button.

Fixes #28809.
---
 .../button-overview-example.css               |  13 +--
 .../button-overview-example.html              |  88 +++++++-------
 src/dev-app/button/button-demo.ts             |   2 +-
 src/material/button/_button-theme.scss        |  52 +++++++++
 src/material/button/button-base.ts            |   2 +-
 src/material/button/button-high-contrast.scss |   1 +
 src/material/button/button.md                 |  10 +-
 src/material/button/button.scss               |  42 ++++++-
 src/material/button/button.ts                 |   1 +
 .../button/testing/button-harness-filters.ts  |   2 +-
 .../button/testing/button-harness.spec.ts     |   7 +-
 src/material/button/testing/button-harness.ts |   4 +
 src/material/core/tokens/_density.scss        |   3 +
 .../core/tokens/_token-definition.scss        |   3 +-
 src/material/core/tokens/m2/_index.scss       |   2 +
 .../core/tokens/m2/mat/_tonal-button.scss     | 110 ++++++++++++++++++
 src/material/core/tokens/m3/_index.scss       |   2 +
 .../core/tokens/m3/definitions/_index.scss    |   2 +-
 .../_md-comp-filled-tonal-button.scss         |   0
 .../core/tokens/m3/mat/_tonal-button.scss     | 101 ++++++++++++++++
 .../material/button-testing.md                |   2 +-
 tools/public_api_guard/material/button.md     |   2 +-
 22 files changed, 379 insertions(+), 72 deletions(-)
 create mode 100644 src/material/core/tokens/m2/mat/_tonal-button.scss
 rename src/material/core/tokens/m3/definitions/{unused => }/_md-comp-filled-tonal-button.scss (100%)
 create mode 100644 src/material/core/tokens/m3/mat/_tonal-button.scss

diff --git a/src/components-examples/material/button/button-overview/button-overview-example.css b/src/components-examples/material/button/button-overview/button-overview-example.css
index 0706c1b66c1d..06631239a9bb 100644
--- a/src/components-examples/material/button/button-overview/button-overview-example.css
+++ b/src/components-examples/material/button/button-overview/button-overview-example.css
@@ -1,16 +1,15 @@
 section {
-  display: table;
+  display: flex;
+  align-items: center;
 }
 
 .example-label {
-  display: table-cell;
   font-size: 14px;
-  margin-left: 8px;
+  margin: 0 16px 0 8px;
   min-width: 120px;
 }
 
 .example-button-row {
-  display: table-cell;
   max-width: 600px;
 }
 
@@ -23,9 +22,3 @@ section {
   justify-content: space-between;
   flex-wrap: wrap;
 }
-
-.example-button-container {
-  display: flex;
-  justify-content: center;
-  width: 140px;
-}
diff --git a/src/components-examples/material/button/button-overview/button-overview-example.html b/src/components-examples/material/button/button-overview/button-overview-example.html
index a8e7332c497d..f2dee64faf0f 100644
--- a/src/components-examples/material/button/button-overview/button-overview-example.html
+++ b/src/components-examples/material/button/button-overview/button-overview-example.html
@@ -6,7 +6,7 @@
     <a matButton href="https://www.google.com/" target="_blank">Link</a>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
 <section>
   <div class="example-label">Elevated</div>
   <div class="example-button-row">
@@ -15,7 +15,7 @@
     <a matButton="elevated" href="https://www.google.com/" target="_blank">Link</a>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
 <section>
   <div class="example-label">Outlined</div>
   <div class="example-button-row">
@@ -24,16 +24,25 @@
     <a matButton="outlined" href="https://www.google.com/" target="_blank">Link</a>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
 <section>
   <div class="example-label">Filled</div>
   <div class="example-button-row">
-    <button matButton="filled" >Basic</button>
-    <button matButton="filled"  disabled>Disabled</button>
+    <button matButton="filled">Basic</button>
+    <button matButton="filled" disabled>Disabled</button>
     <a matButton="filled" href="https://www.google.com/" target="_blank">Link</a>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
+<section>
+  <div class="example-label">Tonal</div>
+  <div class="example-button-row">
+    <button matButton="tonal" >Basic</button>
+    <button matButton="tonal"  disabled>Disabled</button>
+    <a matButton="tonal" href="https://www.google.com/" target="_blank">Link</a>
+  </div>
+</section>
+<mat-divider/>
 <section>
   <div class="example-label">Icon</div>
   <div class="example-button-row">
@@ -47,64 +56,51 @@
     </div>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
 <section>
   <div class="example-label">Floating Action Button (FAB)</div>
   <div class="example-button-row">
     <div class="example-flex-container">
-      <div class="example-button-container">
-        <button matFab aria-label="Example icon button with a delete icon">
-          <mat-icon>delete</mat-icon>
-        </button>
-      </div>
-      <div class="example-button-container">
-        <button matFab disabled aria-label="Example icon button with a heart icon">
-          <mat-icon>favorite</mat-icon>
-        </button>
-      </div>
+      <button matFab aria-label="Example icon button with a delete icon">
+        <mat-icon>delete</mat-icon>
+      </button>
+      <button matFab disabled aria-label="Example icon button with a heart icon">
+        <mat-icon>favorite</mat-icon>
+      </button>
     </div>
   </div>
 </section>
-<mat-divider></mat-divider>
+<mat-divider/>
 <section>
   <div class="example-label">Mini FAB</div>
   <div class="example-button-row">
     <div class="example-flex-container">
-      <div class="example-button-container">
-        <button matMiniFab aria-label="Example icon button with a menu icon">
-          <mat-icon>menu</mat-icon>
-        </button>
-      </div>
-      <div class="example-button-container">
-        <button matMiniFab disabled aria-label="Example icon button with a home icon">
-          <mat-icon>home</mat-icon>
-        </button>
-      </div>
+      <button matMiniFab aria-label="Example icon button with a menu icon">
+        <mat-icon>menu</mat-icon>
+      </button>
+      <button matMiniFab disabled aria-label="Example icon button with a home icon">
+        <mat-icon>home</mat-icon>
+      </button>
     </div>
   </div>
 </section>
+<mat-divider/>
 <section>
   <div class="example-label">Extended FAB</div>
   <div class="example-button-row">
     <div class="example-flex-container">
-      <div class="example-button-container">
-        <button matFab extended>
-          <mat-icon>favorite</mat-icon>
-          Basic
-        </button>
-      </div>
-      <div class="example-button-container">
-        <button matFab extended disabled>
-          <mat-icon>favorite</mat-icon>
-          Disabled
-        </button>
-      </div>
-      <div class="example-button-container">
-        <a matFab extended routerLink=".">
-          <mat-icon>favorite</mat-icon>
-          Link
-        </a>
-      </div>
+      <button matFab extended>
+        <mat-icon>favorite</mat-icon>
+        Basic
+      </button>
+      <button matFab extended disabled>
+        <mat-icon>favorite</mat-icon>
+        Disabled
+      </button>
+      <a matFab extended routerLink=".">
+        <mat-icon>favorite</mat-icon>
+        Link
+      </a>
     </div>
   </div>
 </section>
diff --git a/src/dev-app/button/button-demo.ts b/src/dev-app/button/button-demo.ts
index 809a1214564e..b8b8fa4b42f3 100644
--- a/src/dev-app/button/button-demo.ts
+++ b/src/dev-app/button/button-demo.ts
@@ -49,5 +49,5 @@ export class ButtonDemo {
   toggleDisable = false;
   tooltipText = 'This is a button tooltip!';
   disabledInteractive = false;
-  appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled'];
+  appearances: MatButtonAppearance[] = ['text', 'elevated', 'outlined', 'filled', 'tonal'];
 }
diff --git a/src/material/button/_button-theme.scss b/src/material/button/_button-theme.scss
index 37aeb9f029d3..5ba48ebb0fbf 100644
--- a/src/material/button/_button-theme.scss
+++ b/src/material/button/_button-theme.scss
@@ -11,6 +11,7 @@
 @use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button;
 @use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button;
 @use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button;
+@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button;
 @use '../core/style/sass-utils';
 
 @mixin _text-button-variant($theme, $palette) {
@@ -81,6 +82,15 @@
   @include token-utils.create-token-values(tokens-mat-outlined-button.$prefix, $mat-tokens);
 }
 
+@mixin _tonal-button-variant($theme, $palette) {
+  @include token-utils.create-token-values(tokens-mat-tonal-button.$prefix, if(
+    $palette,
+    tokens-mat-tonal-button.private-get-color-palette-color-tokens($theme, $palette),
+    tokens-mat-tonal-button.get-color-tokens($theme)
+  ));
+}
+
+
 @mixin _theme-from-tokens($tokens, $options...) {
   @include validation.selector-defined(
     'Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector'
@@ -125,6 +135,11 @@
     tokens-mat-outlined-button.$prefix,
     $options...
   );
+  $mat-tonal-button-tokens: token-utils.get-tokens-for(
+    $tokens,
+    tokens-mat-tonal-button.$prefix,
+    $options...
+  );
 
   @include token-utils.create-token-values(tokens-mdc-text-button.$prefix, $mdc-text-button-tokens);
   @include token-utils.create-token-values(
@@ -152,6 +167,10 @@
     tokens-mat-outlined-button.$prefix,
     $mat-outlined-button-tokens
   );
+  @include token-utils.create-token-values(
+    tokens-mat-tonal-button.$prefix,
+    $mat-tonal-button-tokens
+  );
 }
 
 /// Outputs base theme styles (styles not dependent on the color, typography, or density settings)
@@ -195,6 +214,10 @@
         tokens-mat-outlined-button.$prefix,
         tokens-mat-outlined-button.get-unthemable-tokens()
       );
+      @include token-utils.create-token-values(
+        tokens-mat-tonal-button.$prefix,
+        tokens-mat-tonal-button.get-unthemable-tokens()
+      );
     }
   }
 }
@@ -211,6 +234,7 @@
     @include sass-utils.current-selector-or-root() {
       @include _text-button-variant($theme, null);
       @include _filled-button-variant($theme, null);
+      @include _tonal-button-variant($theme, null);
       @include _protected-button-variant($theme, null);
       @include _outlined-button-variant($theme, null);
     }
@@ -270,6 +294,20 @@
         @include _outlined-button-variant($theme, warn);
       }
     }
+
+    .mat-tonal-button {
+      &.mat-primary {
+        @include _tonal-button-variant($theme, primary);
+      }
+
+      &.mat-accent {
+        @include _tonal-button-variant($theme, accent);
+      }
+
+      &.mat-warn {
+        @include _tonal-button-variant($theme, warn);
+      }
+    }
   }
 }
 
@@ -313,6 +351,10 @@
         tokens-mat-outlined-button.$prefix,
         tokens-mat-outlined-button.get-typography-tokens($theme)
       );
+      @include token-utils.create-token-values(
+        tokens-mat-tonal-button.$prefix,
+        tokens-mat-tonal-button.get-typography-tokens($theme)
+      );
     }
   }
 }
@@ -357,6 +399,10 @@
         tokens-mat-outlined-button.$prefix,
         tokens-mat-outlined-button.get-density-tokens($theme)
       );
+      @include token-utils.create-token-values(
+        tokens-mat-tonal-button.$prefix,
+        tokens-mat-tonal-button.get-density-tokens($theme)
+      );
     }
   }
 }
@@ -371,6 +417,7 @@
   $mat-protected-button-tokens: tokens-mat-protected-button.get-token-slots();
   $mdc-text-button-tokens: tokens-mdc-text-button.get-token-slots();
   $mat-text-button-tokens: tokens-mat-text-button.get-token-slots();
+  $mat-tonal-button-tokens: tokens-mat-tonal-button.get-token-slots();
 
   @return (
     (
@@ -413,6 +460,11 @@
       tokens: $mat-text-button-tokens,
       prefix: 'text-',
     ),
+    (
+      namespace: tokens-mat-tonal-button.$prefix,
+      tokens: $mat-tonal-button-tokens,
+      prefix: 'tonal-',
+    ),
   );
 }
 
diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts
index 1514044ace80..271044546c36 100644
--- a/src/material/button/button-base.ts
+++ b/src/material/button/button-base.ts
@@ -28,7 +28,7 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private';
  * Possible appearances for a `MatButton`.
  * See https://m3.material.io/components/buttons/overview
  */
-export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
+export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
 
 /** Object that can be used to configure the default options for the button component. */
 export interface MatButtonConfig {
diff --git a/src/material/button/button-high-contrast.scss b/src/material/button/button-high-contrast.scss
index d8a19f270dc0..5dbb5164e2c1 100644
--- a/src/material/button/button-high-contrast.scss
+++ b/src/material/button/button-high-contrast.scss
@@ -4,6 +4,7 @@
 .mat-mdc-unelevated-button:not(.mdc-button--outlined),
 .mat-mdc-raised-button:not(.mdc-button--outlined),
 .mat-mdc-outlined-button:not(.mdc-button--outlined),
+.mat-mdc-button-base.mat-tonal-button,
 .mat-mdc-icon-button.mat-mdc-icon-button,
 .mat-mdc-outlined-button .mdc-button__ripple {
   @include cdk.high-contrast {
diff --git a/src/material/button/button.md b/src/material/button/button.md
index 5f57cea1c3d5..76b83b2f4743 100644
--- a/src/material/button/button.md
+++ b/src/material/button/button.md
@@ -20,13 +20,13 @@ There are several button variants, each applied as an attribute:
 Additionally, the `matButton` has several appearances that can be set using the `matButton`
 attribute, for example `matButton="outlined"`:
 
-
 | Appearance   | Description                                                                      |
 |--------------|----------------------------------------------------------------------------------|
-| `text`       | Default appearance. Does not have a background until the user interacts with it. |
-| `elevated`   | Has a background color, elevation and rounded corners.                           |
-| `filled`     | Has a flat appearance with rounded corners and no elevation.                     |
-| `outlined`   | Has an outline, rounded corners and a transparent background.                    |
+| `text`       | Default appearance. Text buttons are used for the lowest priority actions, especially when presenting multiple options. |
+| `filled`     | High-emphasis buttons used for final or unblocking actions in a flow, such as saving or confirming. |
+| `tonal`      | Medium-emphasis buttons often used for final or unblocking actions in a flow, but with less visual emphasis than a filled button. |
+| `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. |
 
 
 ### Extended FAB buttons
diff --git a/src/material/button/button.scss b/src/material/button/button.scss
index e82f5305d9d1..c52a44a78d09 100644
--- a/src/material/button/button.scss
+++ b/src/material/button/button.scss
@@ -11,6 +11,7 @@
 @use '../core/tokens/m2/mat/protected-button' as tokens-mat-protected-button;
 @use '../core/tokens/m2/mdc/text-button' as tokens-mdc-text-button;
 @use '../core/tokens/m2/mat/text-button' as tokens-mat-text-button;
+@use '../core/tokens/m2/mat/tonal-button' as tokens-mat-tonal-button;
 
 .mat-mdc-button-base {
   text-decoration: none;
@@ -267,10 +268,48 @@
   }
 }
 
+.mat-tonal-button {
+  $slots: tokens-mat-tonal-button.get-token-slots();
+  transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);
+
+  @include token-utils.use-tokens(tokens-mat-tonal-button.$prefix, $slots) {
+    @include token-utils.create-token-slot(height, container-height);
+    @include token-utils.create-token-slot(font-family, label-text-font);
+    @include token-utils.create-token-slot(font-size, label-text-size);
+    @include token-utils.create-token-slot(letter-spacing, label-text-tracking);
+    @include token-utils.create-token-slot(text-transform, label-text-transform);
+    @include token-utils.create-token-slot(font-weight, label-text-weight);
+    padding: 0 #{token-utils.get-token-variable(horizontal-padding, true)};
+
+    &:not(:disabled) {
+      @include token-utils.create-token-slot(color, label-text-color);
+      @include token-utils.create-token-slot(background-color, container-color);
+    }
+
+    &, .mdc-button__ripple {
+      @include token-utils.create-token-slot(border-radius, container-shape);
+    }
+
+    // We need to re-apply the disabled tokens since MDC uses
+    // `:disabled` which doesn't apply to anchors.
+    @include button-base.mat-private-button-disabled {
+      @include token-utils.create-token-slot(color, disabled-label-text-color);
+      @include token-utils.create-token-slot(background-color, disabled-container-color);
+    }
+  }
+
+  @include button-base.mat-private-button-horizontal-layout(tokens-mat-tonal-button.$prefix,
+    $slots, false);
+  @include button-base.mat-private-button-ripple(tokens-mat-tonal-button.$prefix, $slots);
+  @include button-base.mat-private-button-touch-target(false, tokens-mat-tonal-button.$prefix,
+    $slots);
+}
+
 .mat-mdc-button,
 .mat-mdc-unelevated-button,
 .mat-mdc-raised-button,
-.mat-mdc-outlined-button {
+.mat-mdc-outlined-button,
+.mat-tonal-button {
   @include button-base.mat-private-button-interactive();
   @include style-private.private-animation-noop();
 
@@ -306,6 +345,7 @@
 // For the button element, default inset/offset values are necessary to ensure that
 // the focus indicator is sufficiently contrastive and renders appropriately.
 .mat-mdc-unelevated-button,
+.mat-tonal-button,
 .mat-mdc-raised-button {
   .mat-focus-indicator::before {
     $default-border-width: focus-indicators-private.$default-border-width;
diff --git a/src/material/button/button.ts b/src/material/button/button.ts
index 8883e1d27664..f6146519acc3 100644
--- a/src/material/button/button.ts
+++ b/src/material/button/button.ts
@@ -18,6 +18,7 @@ const APPEARANCE_CLASSES: Map<MatButtonAppearance, readonly string[]> = new Map(
   ['filled', ['mdc-button--unelevated', 'mat-mdc-unelevated-button']],
   ['elevated', ['mdc-button--raised', 'mat-mdc-raised-button']],
   ['outlined', ['mdc-button--outlined', 'mat-mdc-outlined-button']],
+  ['tonal', ['mat-tonal-button']],
 ]);
 
 /**
diff --git a/src/material/button/testing/button-harness-filters.ts b/src/material/button/testing/button-harness-filters.ts
index 3ecba00636b2..7f053c133ad0 100644
--- a/src/material/button/testing/button-harness-filters.ts
+++ b/src/material/button/testing/button-harness-filters.ts
@@ -12,7 +12,7 @@ import {BaseHarnessFilters} from '@angular/cdk/testing';
 export type ButtonVariant = 'basic' | 'icon' | 'fab' | 'mini-fab';
 
 /** Possible button appearances. */
-export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
+export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
 
 /** A set of criteria that can be used to filter a list of button harness instances. */
 export interface ButtonHarnessFilters extends BaseHarnessFilters {
diff --git a/src/material/button/testing/button-harness.spec.ts b/src/material/button/testing/button-harness.spec.ts
index fe13608d7a4a..7f3ccda51ef8 100644
--- a/src/material/button/testing/button-harness.spec.ts
+++ b/src/material/button/testing/button-harness.spec.ts
@@ -22,7 +22,7 @@ describe('MatButtonHarness', () => {
 
   it('should load all button harnesses', async () => {
     const buttons = await loader.getAllHarnesses(MatButtonHarness);
-    expect(buttons.length).toBe(15);
+    expect(buttons.length).toBe(16);
   });
 
   it('should load button with exact text', async () => {
@@ -41,7 +41,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(13);
+    expect(enabledButtons.length).toBe(14);
     expect(disabledButtons.length).toBe(2);
   });
 
@@ -118,6 +118,7 @@ describe('MatButtonHarness', () => {
       'basic',
       'basic',
       'basic',
+      'basic',
       'icon',
       'icon',
       'fab',
@@ -141,6 +142,7 @@ describe('MatButtonHarness', () => {
       'filled',
       'elevated',
       'outlined',
+      'tonal',
       null,
       null,
       null,
@@ -178,6 +180,7 @@ describe('MatButtonHarness', () => {
     </button>
     <button id="raised" type="button" matButton="elevated">Elevated button</button>
     <button id="stroked" type="button" matButton="outlined">Outlined button</button>
+    <button id="tonal" type="button" matButton="tonal">Tonal button</button>
     <button id="home-icon" type="button" matIconButton>
       <mat-icon>home</mat-icon>
     </button>
diff --git a/src/material/button/testing/button-harness.ts b/src/material/button/testing/button-harness.ts
index b711387e7fd5..e99545d493bb 100644
--- a/src/material/button/testing/button-harness.ts
+++ b/src/material/button/testing/button-harness.ts
@@ -140,6 +140,10 @@ export class MatButtonHarness extends ContentContainerComponentHarness {
       return 'text';
     }
 
+    if (await host.hasClass('mat-tonal-button')) {
+      return 'tonal';
+    }
+
     return null;
   }
 }
diff --git a/src/material/core/tokens/_density.scss b/src/material/core/tokens/_density.scss
index 5bc573138c0c..11500bcc4157 100644
--- a/src/material/core/tokens/_density.scss
+++ b/src/material/core/tokens/_density.scss
@@ -38,6 +38,9 @@ $_density-tokens: (
   (mdc, filled-button): (
     container-height: (40px, 36px, 32px, 28px),
   ),
+  (mat, tonal-button): (
+    container-height: (40px, 36px, 32px, 28px),
+  ),
   (mdc, outlined-button): (
     container-height: (40px, 36px, 32px, 28px),
   ),
diff --git a/src/material/core/tokens/_token-definition.scss b/src/material/core/tokens/_token-definition.scss
index aa4803556ca6..102d7aba5684 100644
--- a/src/material/core/tokens/_token-definition.scss
+++ b/src/material/core/tokens/_token-definition.scss
@@ -173,8 +173,7 @@ $_system-fallbacks: null;
     @if(meta.type-of($color) == 'color') {
       $result: map.remove($result, $opacity-key);
       $result: map.set($result, $color-key, rgba($color, $opacity));
-    }
-    @else if($color != null) {
+    } @else if($color != null) {
       $result: map.remove($result, $opacity-key);
       $combined-color: #{color-mix(in srgb, #{$color} #{($opacity * 100) + '%'}, transparent)};
       $result: map.set($result, $color-key, $combined-color);
diff --git a/src/material/core/tokens/m2/_index.scss b/src/material/core/tokens/m2/_index.scss
index 85c8c7195b50..ada2482ff850 100644
--- a/src/material/core/tokens/m2/_index.scss
+++ b/src/material/core/tokens/m2/_index.scss
@@ -45,6 +45,7 @@
 @use './mat/toolbar' as tokens-mat-toolbar;
 @use './mat/tree' as tokens-mat-tree;
 @use './mat/timepicker' as tokens-mat-timepicker;
+@use './mat/tonal-button' as tokens-mat-tonal-button;
 @use './mdc/checkbox' as tokens-mdc-checkbox;
 @use './mdc/text-button' as tokens-mdc-text-button;
 @use './mdc/protected-button' as tokens-mdc-protected-button;
@@ -158,6 +159,7 @@
     _get-tokens-for-module($theme, tokens-mat-toolbar),
     _get-tokens-for-module($theme, tokens-mat-tree),
     _get-tokens-for-module($theme, tokens-mat-timepicker),
+    _get-tokens-for-module($theme, tokens-mat-tonal-button),
     _get-tokens-for-module($theme, tokens-mdc-checkbox),
     _get-tokens-for-module($theme, tokens-mdc-chip),
     _get-tokens-for-module($theme, tokens-mdc-circular-progress),
diff --git a/src/material/core/tokens/m2/mat/_tonal-button.scss b/src/material/core/tokens/m2/mat/_tonal-button.scss
new file mode 100644
index 000000000000..d7df7064bbfd
--- /dev/null
+++ b/src/material/core/tokens/m2/mat/_tonal-button.scss
@@ -0,0 +1,110 @@
+@use 'sass:map';
+@use '../../token-definition';
+@use '../../../theming/theming';
+@use '../../../theming/inspection';
+@use '../../../style/sass-utils';
+
+// The prefix used to generate the fully qualified name for tokens in this file.
+$prefix: (mat, tonal-button);
+
+// Tokens that can't be configured through Angular Material's current theming API,
+// but may be in a future version of the theming API.
+@function get-unthemable-tokens() {
+  @return (
+    // Shape of the container element.
+    container-shape: 4px,
+
+    // Start/end padding of the button.
+    horizontal-padding: 16px,
+
+    // Space between the icon and the button's main content.
+    icon-spacing: 8px,
+
+    // Amount by which to offset the icon so that its presence
+    // doesn't increase throw off the horizontal padding.
+    icon-offset: -4px,
+  );
+}
+
+// Tokens that can be configured through Angular Material's color theming API.
+@function get-color-tokens($theme) {
+  $is-dark: inspection.get-theme-type($theme) == dark;
+
+  @return (
+    container-color: inspection.get-theme-color($theme, background, card),
+    label-text-color: inspection.get-theme-color($theme, foreground, text, 1),
+    disabled-container-color: inspection.get-theme-color($theme, foreground, disabled-button,
+      0.12),
+    disabled-label-text-color: inspection.get-theme-color($theme, foreground, disabled-button,
+      if($is-dark, 0.5, 0.38)),
+
+    // Color of the element that shows the hover, focus and pressed states.
+    state-layer-color: inspection.get-theme-color($theme, foreground, base),
+
+    // Color of the element that shows the hover, focus and pressed states while disabled.
+    disabled-state-layer-color: inspection.get-theme-color($theme, foreground, base),
+
+    // Color of the ripple element.
+    ripple-color: inspection.get-theme-color($theme, foreground, base, 0.1),
+
+    // Opacity of the ripple when the button is hovered.
+    hover-state-layer-opacity: if($is-dark, 0.08, 0.04),
+
+    // Opacity of the ripple when the button is focused.
+    focus-state-layer-opacity: if($is-dark, 0.24, 0.12),
+
+    // Opacity of the ripple when the button is pressed.
+    pressed-state-layer-opacity: if($is-dark, 0.24, 0.12),
+  );
+}
+
+// Generates the mapping for the properties that change based on the button palette color.
+@function private-get-color-palette-color-tokens($theme, $palette-name) {
+  @return (
+    container-color: inspection.get-theme-color($theme, $palette-name, default),
+    label-text-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1),
+    state-layer-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 1),
+    ripple-color: inspection.get-theme-color($theme, $palette-name, default-contrast, 0.1),
+  );
+}
+
+// Tokens that can be configured through Angular Material's typography theming API.
+@function get-typography-tokens($theme) {
+  @return (
+    label-text-font: inspection.get-theme-typography($theme, button, font-family),
+    label-text-size: inspection.get-theme-typography($theme, button, font-size),
+    label-text-tracking: inspection.get-theme-typography($theme, button, letter-spacing),
+    label-text-weight: inspection.get-theme-typography($theme, button, font-weight),
+    label-text-transform: none,
+  );
+}
+
+// Tokens that can be configured through Angular Material's density theming API.
+@function get-density-tokens($theme) {
+  $scale: theming.clamp-density(inspection.get-theme-density($theme), -3);
+
+  @return (
+    touch-target-display: if($scale < -1, none, block),
+    container-height:
+      map.get(
+        (
+          0: 36px,
+          -1: 32px,
+          -2: 28px,
+          -3: 24px,
+        ),
+        $scale
+      )
+  );
+}
+
+// Combines the tokens generated by the above functions into a single map with placeholder values.
+// This is used to create token slots.
+@function get-token-slots() {
+  @return sass-utils.deep-merge-all(
+      get-unthemable-tokens(),
+      get-color-tokens(token-definition.$placeholder-color-config),
+      get-typography-tokens(token-definition.$placeholder-typography-config),
+      get-density-tokens(token-definition.$placeholder-density-config)
+  );
+}
diff --git a/src/material/core/tokens/m3/_index.scss b/src/material/core/tokens/m3/_index.scss
index 4d85b20f002a..d7c8835c1586 100644
--- a/src/material/core/tokens/m3/_index.scss
+++ b/src/material/core/tokens/m3/_index.scss
@@ -43,6 +43,7 @@
 @use './mat/toolbar' as tokens-mat-toolbar;
 @use './mat/tree' as tokens-mat-tree;
 @use './mat/timepicker' as tokens-mat-timepicker;
+@use './mat/tonal-button' as tokens-mat-tonal-button;
 @use './mdc/checkbox' as tokens-mdc-checkbox;
 @use './mdc/text-button' as tokens-mdc-text-button;
 @use './mdc/protected-button' as tokens-mdc-protected-button;
@@ -85,6 +86,7 @@ $_module-names: (
   tokens-mat-fab,
   tokens-mat-fab-small,
   tokens-mat-filled-button,
+  tokens-mat-tonal-button,
   tokens-mat-form-field,
   tokens-mat-grid-list,
   tokens-mat-icon-button,
diff --git a/src/material/core/tokens/m3/definitions/_index.scss b/src/material/core/tokens/m3/definitions/_index.scss
index bd05e31547b5..60eb8adf886f 100644
--- a/src/material/core/tokens/m3/definitions/_index.scss
+++ b/src/material/core/tokens/m3/definitions/_index.scss
@@ -14,6 +14,7 @@
 @forward './md-comp-fab-tertiary-small' as md-comp-fab-tertiary-small-*;
 @forward './md-comp-fab-tertiary' as md-comp-fab-tertiary-*;
 @forward './md-comp-filled-button' as md-comp-filled-button-*;
+@forward './md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*;
 @forward './md-comp-filled-card' as md-comp-filled-card-*;
 @forward './md-comp-filled-icon-button' as md-comp-filled-icon-button-*;
 @forward './md-comp-filled-text-field' as md-comp-filled-text-field-*;
@@ -63,7 +64,6 @@
 // @forward './unused/md-comp-filled-autocomplete' as md-comp-filled-autocomplete-*;
 // @forward './unused/md-comp-filled-menu-button' as md-comp-filled-menu-button-*;
 // @forward './unused/md-comp-filled-select' as md-comp-filled-select-*;
-// @forward './unused/md-comp-filled-tonal-button' as md-comp-filled-tonal-button-*;
 // @forward './unused/md-comp-filled-tonal-icon-button' as md-comp-filled-tonal-icon-button-*;
 // @forward './unused/md-comp-filter-chip' as md-comp-filter-chip-*;
 // @forward './unused/md-comp-full-screen-dialog' as md-comp-full-screen-dialog-*;
diff --git a/src/material/core/tokens/m3/definitions/unused/_md-comp-filled-tonal-button.scss b/src/material/core/tokens/m3/definitions/_md-comp-filled-tonal-button.scss
similarity index 100%
rename from src/material/core/tokens/m3/definitions/unused/_md-comp-filled-tonal-button.scss
rename to src/material/core/tokens/m3/definitions/_md-comp-filled-tonal-button.scss
diff --git a/src/material/core/tokens/m3/mat/_tonal-button.scss b/src/material/core/tokens/m3/mat/_tonal-button.scss
new file mode 100644
index 000000000000..0069c240db12
--- /dev/null
+++ b/src/material/core/tokens/m3/mat/_tonal-button.scss
@@ -0,0 +1,101 @@
+@use 'sass:map';
+@use 'sass:meta';
+@use '../../../style/sass-utils';
+@use '../../token-definition';
+
+// The prefix used to generate the fully qualified name for tokens in this file.
+$prefix: (mat, tonal-button);
+
+/// Generates custom tokens for the mat-flat-button.
+/// @param {Map} $systems The MDC system tokens
+/// @param {Boolean} $exclude-hardcoded Whether to exclude hardcoded token values
+/// @param {Map} $token-slots Possible token slots
+/// @return {Map} A set of custom tokens for the mat-flat-button
+@function get-tokens($systems, $exclude-hardcoded, $token-slots) {
+  $mdc-tokens: token-definition.get-mdc-tokens('filled-tonal-button', $systems, $exclude-hardcoded);
+
+  $tokens: map.merge($mdc-tokens, (
+    horizontal-padding: token-definition.hardcode(24px, $exclude-hardcoded),
+    icon-spacing: token-definition.hardcode(8px, $exclude-hardcoded),
+    icon-offset: token-definition.hardcode(-8px, $exclude-hardcoded),
+    state-layer-color: map.get($systems, md-sys-color, on-secondary-container),
+    disabled-state-layer-color: map.get($systems, md-sys-color, on-surface-variant),
+    ripple-color: sass-utils.safe-color-change(
+      map.get($systems, md-sys-color, on-secondary-container),
+      $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
+    ),
+    hover-state-layer-opacity: map.get($systems, md-sys-state, hover-state-layer-opacity),
+    focus-state-layer-opacity: map.get($systems, md-sys-state, focus-state-layer-opacity),
+    pressed-state-layer-opacity: map.get($systems, md-sys-state, pressed-state-layer-opacity),
+  ));
+
+  $variant-tokens: (
+    // Color variants:
+    primary: (),
+    secondary: (),
+    tertiary: (
+      container-color: map.get($systems, md-sys-color, tertiary-container),
+      focus-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
+      focus-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
+      hover-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
+      hover-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
+      label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
+      pressed-label-text-color: map.get($systems, md-sys-color, on-tertiary-container),
+      pressed-state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
+      with-icon-focus-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
+      with-icon-hover-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
+      with-icon-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
+      with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-tertiary-container),
+      state-layer-color: map.get($systems, md-sys-color, on-tertiary-container),
+      ripple-color: sass-utils.safe-color-change(
+        map.get($systems, md-sys-color, on-tertiary-container),
+        $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
+      ),
+    ),
+    error: (
+      container-color: map.get($systems, md-sys-color, error-container),
+      focus-label-text-color: map.get($systems, md-sys-color, on-error-container),
+      focus-state-layer-color: map.get($systems, md-sys-color, on-error-container),
+      hover-label-text-color: map.get($systems, md-sys-color, on-error-container),
+      hover-state-layer-color: map.get($systems, md-sys-color, on-error-container),
+      label-text-color: map.get($systems, md-sys-color, on-error-container),
+      pressed-label-text-color: map.get($systems, md-sys-color, on-error-container),
+      pressed-state-layer-color: map.get($systems, md-sys-color, on-error-container),
+      with-icon-focus-icon-color: map.get($systems, md-sys-color, on-error-container),
+      with-icon-hover-icon-color: map.get($systems, md-sys-color, on-error-container),
+      with-icon-icon-color: map.get($systems, md-sys-color, on-error-container),
+      with-icon-pressed-icon-color: map.get($systems, md-sys-color, on-error-container),
+      state-layer-color: map.get($systems, md-sys-color, on-error-container),
+      ripple-color: sass-utils.safe-color-change(
+        map.get($systems, md-sys-color, on-error-container),
+        $alpha: map.get($systems, md-sys-state, pressed-state-layer-opacity)
+      ),
+    )
+  );
+
+  @return token-definition.namespace-tokens($prefix, (
+    _fix-tokens($tokens),
+    token-definition.map-values($variant-tokens, meta.get-function(_fix-tokens))
+  ), $token-slots);
+}
+
+
+/// Fixes inconsistent values in the tonal button tokens so that they can produce valid styles.
+/// @param {Map} $initial-tokens Map of tonal button tokens currently being generated.
+/// @return {Map} The given tokens, with the invalid values replaced with valid ones.
+@function _fix-tokens($initial-tokens) {
+  // Need to get the hardcoded values, because they include opacities that are used for the disabled
+  // state.
+  $hardcoded-tokens: token-definition.get-mdc-tokens('filled-tonal-button', (), false);
+
+  @return token-definition.combine-color-tokens($initial-tokens, $hardcoded-tokens, (
+    (
+      color: disabled-label-text-color,
+      opacity: disabled-label-text-opacity,
+    ),
+    (
+      color: disabled-container-color,
+      opacity: disabled-container-opacity,
+    )
+  ));
+}
diff --git a/tools/public_api_guard/material/button-testing.md b/tools/public_api_guard/material/button-testing.md
index 7f7bf13f38db..b8d5746b1f94 100644
--- a/tools/public_api_guard/material/button-testing.md
+++ b/tools/public_api_guard/material/button-testing.md
@@ -10,7 +10,7 @@ import { ContentContainerComponentHarness } from '@angular/cdk/testing';
 import { HarnessPredicate } from '@angular/cdk/testing';
 
 // @public
-export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
+export type ButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
 
 // @public
 export interface ButtonHarnessFilters extends BaseHarnessFilters {
diff --git a/tools/public_api_guard/material/button.md b/tools/public_api_guard/material/button.md
index 508b149edf41..30516a1be890 100644
--- a/tools/public_api_guard/material/button.md
+++ b/tools/public_api_guard/material/button.md
@@ -44,7 +44,7 @@ export class MatButton extends MatButtonBase {
 }
 
 // @public
-export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined';
+export type MatButtonAppearance = 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal';
 
 // @public
 export interface MatButtonConfig {