diff --git a/README.md b/README.md
index c660fd16b2e..7ec039e7bb8 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ npm install @brightspace-ui/core
* [Alert](components/alert/): alert components for displaying important information
* [Breadcrumbs](components/breadcrumbs/): component to help users understand where they are within an application
* [Backdrop](components/backdrop/): component for displaying backdrop behind a target element
- * [Buttons](components/button/): normal, primary, icon and subtle buttons
+ * [Buttons](components/button/): normal, primary, icon, subtle, and toggle buttons
* [Calendar](components/calendar/): calendar component
* [Card](components/card/): card components
* [Colors](components/colors/): color palette
diff --git a/components/button/README.md b/components/button/README.md
index 656900a5375..5cf0ddf5b33 100644
--- a/components/button/README.md
+++ b/components/button/README.md
@@ -143,6 +143,34 @@ The `d2l-button-icon` element can be used just like the native `button`, for ins
```
+## Toggle Button [d2l-button-toggle]
+
+The `d2l-button-toggle` element is a container for buttons that toggle a `pressed` state. The component will automatically show or hide the buttons and manage focus based on the `pressed` state. Simply place a `d2l-button-icon` or `d2l-button-subtle` element in each of the `not-pressed` and `pressed` slots. Each button should describe the state and action the user can take.
+
+
+```html
+
+
+
+
+
+```
+
+
+### Properties
+
+| Property | Type | Description |
+|--|--|--|
+| `pressed` | Boolean | Pressed state |
+
+### Events
+
+- `d2l-button-toggle-change`: dispatched when the `pressed` state changes
+
+
## Add Button [d2l-button-add]
The `d2l-button-add` is for quickly adding new items at a specific location, such as when adding items to a curated list. Since the Add button is meant to be subtle, it should always be used in combination with more obvious methods to add items (like a menu or primary button).
@@ -220,6 +248,9 @@ Daylight buttons rely on standard button semantics to ensure a smooth experience
* For [Icon Buttons](#d2l-button-icon) where there is no visible label, `text` will be displayed in a tooltip
* If both `text` and `aria-label` are used, then `aria-label` will be used as the primary label while `text` will be used in a [tooltip](../../components/tooltip)
+* [Toggle buttons](#d2l-button-toggle) should describe the current state and the action the user can perform. As such, `aria-pressed` should not be used on the buttons as per [W3C's Button Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/#:~:text=Alternatively%2C%20if%20the%20design%20were%20to%20call%20for%20the%20button%20label%20to%20change%20from%20%22Mute%22%20to%20%22Unmute%2C%22%20the%20aria%2Dpressed%20attribute%20would%20not%20be%20needed.).
+ * Example: "Unpinned, click to pin" and "Pinned, click to unpin"
+
* [Floating Buttons](#d2l-floating-buttons) maintain their position in the document's structure, despite sticking to the bottom of the viewport, so the tab order is unaffected and the effect is imperceptible to screen reader users
* Be cautious when using `always-float`, since screen magnifier users may find it difficult to locate the buttons at the bottom of a large viewport
diff --git a/components/button/button-toggle.js b/components/button/button-toggle.js
new file mode 100644
index 00000000000..ae81c4f9a63
--- /dev/null
+++ b/components/button/button-toggle.js
@@ -0,0 +1,97 @@
+import { css, html, LitElement } from 'lit';
+
+/**
+ * A button container component for button toggles.
+ */
+class ButtonToggle extends LitElement {
+
+ static get properties() {
+ return {
+ /**
+ * Pressed state
+ * @type {boolean}
+ */
+ pressed: { type: Boolean, reflect: true }
+ };
+ }
+
+ static get styles() {
+ return css`
+ :host {
+ display: inline-block;
+ }
+ :host([hidden]) {
+ display: none;
+ }
+ ::slotted(:not(d2l-button-icon, d2l-button-subtle)),
+ :host slot[name="pressed"],
+ :host([pressed]) slot[name="not-pressed"] {
+ display: none;
+ }
+ :host slot[name="not-pressed"],
+ :host([pressed]) slot[name="pressed"] {
+ display: contents;
+ }
+ `;
+ }
+
+ constructor() {
+ super();
+ this.pressed = false;
+ }
+
+ firstUpdated(changedProperties) {
+ super.firstUpdated(changedProperties);
+ if (this._focusOnFirstRender) {
+ this._focusOnFirstRender = false;
+ this.focus();
+ }
+ }
+
+ render() {
+ return html`
+
+
+ `;
+ }
+
+ updated(changedProperties) {
+ super.updated(changedProperties);
+
+ if (changedProperties.get('pressed') === undefined) return;
+
+ /** Dispatched when the pressed state changes */
+ this.dispatchEvent(new CustomEvent('d2l-button-toggle-change'));
+ }
+
+ focus() {
+ if (!this.hasUpdated) {
+ this._focusOnFirstRender = true;
+ return;
+ }
+
+ const elem = this.shadowRoot.querySelector(this.pressed ? 'slot[name="pressed"]' : 'slot[name="not-pressed"]').assignedNodes()[0];
+ if (!elem) {
+ throw new Error('d2l-button-toggle: no button to focus');
+ }
+
+ elem.focus();
+ }
+
+ async _handleClick(pressed) {
+ this.pressed = pressed;
+ await this.updateComplete;
+ this.focus();
+ }
+
+ _handleNotPressedClick() {
+ this._handleClick(true);
+ }
+
+ _handlePressedClick() {
+ this._handleClick(false);
+ }
+
+}
+
+customElements.define('d2l-button-toggle', ButtonToggle);
diff --git a/components/button/demo/button-toggle.html b/components/button/demo/button-toggle.html
new file mode 100644
index 00000000000..cef6079c494
--- /dev/null
+++ b/components/button/demo/button-toggle.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Toggle Button (using d2l-button-icon)
+
+
+
+
+
+
+
+
+
+
+
+
Toggle Button (using d2l-button-subtle)
+
+
+
+
+
+
+
+
+
+
+
+
Toggle Button (disabled)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/button/test/button-toggle.axe.js b/components/button/test/button-toggle.axe.js
new file mode 100644
index 00000000000..52469e53d94
--- /dev/null
+++ b/components/button/test/button-toggle.axe.js
@@ -0,0 +1,26 @@
+import '../button-icon.js';
+import '../button-toggle.js';
+import { expect, fixture, html } from '@brightspace-ui/testing';
+
+describe('d2l-button-toggle', () => {
+
+ const normalFixture = html`
+
+
+
+
+ `;
+
+ it('not pressed', async() => {
+ const el = await fixture(normalFixture);
+ await expect(el).to.be.accessible();
+ });
+
+ it('pressed', async() => {
+ const el = await fixture(normalFixture);
+ el.pressed = true;
+ await el.updateComplete;
+ await expect(el).to.be.accessible();
+ });
+
+});
diff --git a/components/button/test/button-toggle.test.js b/components/button/test/button-toggle.test.js
new file mode 100644
index 00000000000..9b3a12e6fbe
--- /dev/null
+++ b/components/button/test/button-toggle.test.js
@@ -0,0 +1,66 @@
+import '../button-icon.js';
+import '../button-toggle.js';
+import { clickElem, expect, fixture, html, oneEvent, runConstructor } from '@brightspace-ui/testing';
+
+describe('d2l-button-toggle', () => {
+
+ describe('constructor', () => {
+
+ it('should construct', () => {
+ runConstructor('d2l-button-toggle');
+ });
+
+ });
+
+ describe('events', () => {
+
+ it('dispatches d2l-button-toggle-change event not-pressed is clicked', async() => {
+ const el = await fixture(html`
+
+
+
+
+ `);
+ clickElem(el.querySelector('[slot="not-pressed"]'));
+ const e = await oneEvent(el, 'd2l-button-toggle-change');
+ expect(e.target.pressed).to.equal(true);
+ });
+
+ it('dispatches d2l-button-toggle-change event pressed is clicked', async() => {
+ const el = await fixture(html`
+
+
+
+
+ `);
+ clickElem(el.querySelector('[slot="pressed"]'));
+ const e = await oneEvent(el, 'd2l-button-toggle-change');
+ expect(e.target.pressed).to.equal(false);
+ });
+
+ it('does not dispatch d2l-button-toggle-change event initially', async() => {
+ let dispatched = false;
+ const el = document.createElement('d2l-button-toggle');
+ el.addEventListener('d2l-button-toggle-change', () => dispatched = true);
+ document.body.appendChild(el);
+ await el.updateComplete;
+ expect(dispatched).to.equal(false);
+ });
+
+ it('does not dispatch d2l-button-toggle-change event if disabled buttons are clicked', async() => {
+ const el = await fixture(html`
+
+
+
+
+ `);
+ let dispatched = false;
+ el.addEventListener('d2l-button-toggle-change', () => dispatched = true);
+ await clickElem(el.querySelector('[slot="not-pressed"]'));
+ expect(el.pressed).to.equal(false);
+ expect(dispatched).to.be.false;
+ });
+
+ });
+
+});
diff --git a/components/button/test/button-toggle.vdiff.js b/components/button/test/button-toggle.vdiff.js
new file mode 100644
index 00000000000..77440e84cfd
--- /dev/null
+++ b/components/button/test/button-toggle.vdiff.js
@@ -0,0 +1,39 @@
+import '../button-icon.js';
+import '../button-subtle.js';
+import '../button-toggle.js';
+import { clickElem, expect, fixture, focusElem, hoverElem, html, sendKeysElem } from '@brightspace-ui/testing';
+
+describe('button-toggle', () => {
+
+ [
+ { category: 'button-icon', template: html`` },
+ { category: 'button-icon-pressed', template: html`` },
+ { category: 'button-subtle', template: html`` },
+ { category: 'button-subtle-pressed', template: html`` },
+ { category: 'button-subtle-disabled', template: html`` }
+ ].forEach(({ category, template }) => {
+
+ const getActiveButton = elem => {
+ if (elem.pressed) return elem.querySelector('[slot="pressed"]');
+ else return elem.querySelector('[slot="not-pressed"]');
+ };
+
+ describe(category, () => {
+ [
+ { name: 'normal' },
+ { name: 'hover', action: hoverElem },
+ { name: 'focus', action: focusElem },
+ { name: 'click', action: elem => clickElem(getActiveButton(elem)) },
+ { name: 'enter', action: elem => sendKeysElem(getActiveButton(elem), 'press', 'Enter') }
+ ].forEach(({ action, name }) => {
+ it(name, async() => {
+ const elem = await fixture(template);
+ if (action) await action(elem);
+ await expect(elem).to.be.golden();
+ });
+ });
+ });
+
+ });
+
+});
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-click.png b/components/button/test/golden/button-toggle/chromium/button-icon-click.png
new file mode 100644
index 00000000000..8b30318e4af
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-click.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-enter.png b/components/button/test/golden/button-toggle/chromium/button-icon-enter.png
new file mode 100644
index 00000000000..900624a3a5f
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-enter.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-focus.png b/components/button/test/golden/button-toggle/chromium/button-icon-focus.png
new file mode 100644
index 00000000000..55935ec0ee5
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-focus.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-hover.png b/components/button/test/golden/button-toggle/chromium/button-icon-hover.png
new file mode 100644
index 00000000000..0fd7e0e6e80
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-hover.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-normal.png b/components/button/test/golden/button-toggle/chromium/button-icon-normal.png
new file mode 100644
index 00000000000..8dadaf06c63
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-normal.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-pressed-click.png b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-click.png
new file mode 100644
index 00000000000..0fd7e0e6e80
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-click.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-pressed-enter.png b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-enter.png
new file mode 100644
index 00000000000..55935ec0ee5
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-enter.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-pressed-focus.png b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-focus.png
new file mode 100644
index 00000000000..900624a3a5f
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-focus.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-pressed-hover.png b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-hover.png
new file mode 100644
index 00000000000..8b30318e4af
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-hover.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-icon-pressed-normal.png b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-normal.png
new file mode 100644
index 00000000000..a3b17e3ccce
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-icon-pressed-normal.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-click.png b/components/button/test/golden/button-toggle/chromium/button-subtle-click.png
new file mode 100644
index 00000000000..9ed9d43a5b4
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-click.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-click.png b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-click.png
new file mode 100644
index 00000000000..ad9db963b36
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-click.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-enter.png b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-enter.png
new file mode 100644
index 00000000000..ad9db963b36
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-enter.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-focus.png b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-focus.png
new file mode 100644
index 00000000000..ad9db963b36
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-focus.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-hover.png b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-hover.png
new file mode 100644
index 00000000000..ad9db963b36
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-hover.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-normal.png b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-normal.png
new file mode 100644
index 00000000000..ad9db963b36
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-disabled-normal.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-enter.png b/components/button/test/golden/button-toggle/chromium/button-subtle-enter.png
new file mode 100644
index 00000000000..8031d04e32e
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-enter.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-focus.png b/components/button/test/golden/button-toggle/chromium/button-subtle-focus.png
new file mode 100644
index 00000000000..51b80df3742
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-focus.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-hover.png b/components/button/test/golden/button-toggle/chromium/button-subtle-hover.png
new file mode 100644
index 00000000000..71b5d82a107
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-hover.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-normal.png b/components/button/test/golden/button-toggle/chromium/button-subtle-normal.png
new file mode 100644
index 00000000000..5de3937fc5c
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-normal.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-click.png b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-click.png
new file mode 100644
index 00000000000..71b5d82a107
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-click.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-enter.png b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-enter.png
new file mode 100644
index 00000000000..51b80df3742
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-enter.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-focus.png b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-focus.png
new file mode 100644
index 00000000000..8031d04e32e
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-focus.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-hover.png b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-hover.png
new file mode 100644
index 00000000000..9ed9d43a5b4
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-hover.png differ
diff --git a/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-normal.png b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-normal.png
new file mode 100644
index 00000000000..2c3dc25230b
Binary files /dev/null and b/components/button/test/golden/button-toggle/chromium/button-subtle-pressed-normal.png differ
diff --git a/index.html b/index.html
index 5209d4f9f35..52b407f0453 100644
--- a/index.html
+++ b/index.html
@@ -39,6 +39,7 @@