+))}
\ No newline at end of file
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.js b/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.js
new file mode 100644
index 00000000000000..bd686cbd95a1ed
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.js
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+
+const CustomDivButton = React.forwardRef(function CustomDivButton(props, ref) {
+ return ;
+});
+
+export default function ToggleButtonA11yNonNative() {
+ return (
+
+
+ Non-native pressed
+
+
+ Non-native
+
+
+ Disabled non-native
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.tsx b/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.tsx
new file mode 100644
index 00000000000000..ab0f29bf6ab046
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11yNonNative.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+
+const CustomDivButton = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(function CustomDivButton(props, ref) {
+ return ;
+});
+
+export default function ToggleButtonA11yNonNative() {
+ return (
+
+
+ Non-native pressed
+
+
+ Non-native
+
+
+ Disabled non-native
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.js b/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.js
new file mode 100644
index 00000000000000..7aab07f583499d
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.js
@@ -0,0 +1,40 @@
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+import FormatBoldIcon from '@mui/icons-material/FormatBold';
+
+export default function ToggleButtonA11ySemanticStates() {
+ return (
+
+
+
+ Not pressed
+
+
+ Pressed
+
+
+ Disabled
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.tsx b/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.tsx
new file mode 100644
index 00000000000000..7aab07f583499d
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11ySemanticStates.tsx
@@ -0,0 +1,40 @@
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+import FormatBoldIcon from '@mui/icons-material/FormatBold';
+
+export default function ToggleButtonA11ySemanticStates() {
+ return (
+
+
+
+ Not pressed
+
+
+ Pressed
+
+
+ Disabled
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.js b/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.js
new file mode 100644
index 00000000000000..52968c8cf35b98
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.js
@@ -0,0 +1,31 @@
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+
+export default function ToggleButtonA11yTextSpacing() {
+ return (
+
+
+ Review accessibility settings before continuing
+
+
+
+ Align a longer label to the left
+
+
+ Align a longer label to the right
+
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.tsx b/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.tsx
new file mode 100644
index 00000000000000..52968c8cf35b98
--- /dev/null
+++ b/docs/data/material/components/toggle-button/ToggleButtonA11yTextSpacing.tsx
@@ -0,0 +1,31 @@
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import ToggleButton from '@mui/material/ToggleButton';
+
+export default function ToggleButtonA11yTextSpacing() {
+ return (
+
+
+ Review accessibility settings before continuing
+
+
+
+ Align a longer label to the left
+
+
+ Align a longer label to the right
+
+
+
+ );
+}
diff --git a/docs/data/material/components/toggle-button/toggle-button.a11y.json b/docs/data/material/components/toggle-button/toggle-button.a11y.json
new file mode 100644
index 00000000000000..65d9bed02959a6
--- /dev/null
+++ b/docs/data/material/components/toggle-button/toggle-button.a11y.json
@@ -0,0 +1,334 @@
+{
+ "ToggleButtonA11yColorMatrix": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "color-contrast": {
+ "status": "fail",
+ "tags": ["wcag2aa"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "ToggleButtonA11yNonNative": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-command-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-deprecated-role": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-required-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-roles": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "color-contrast": {
+ "status": "pass",
+ "tags": ["wcag2aa"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "ToggleButtonA11ySemanticStates": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-hidden-focus": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "color-contrast": {
+ "status": "pass",
+ "tags": ["wcag2aa"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "ToggleButtonA11yTextSpacing": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "color-contrast": {
+ "status": "pass",
+ "tags": ["wcag2aa"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "ToggleButtons": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-deprecated-role": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-hidden-focus": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-required-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-roles": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "ToggleButtonsMultiple": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-deprecated-role": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-hidden-focus": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-required-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-roles": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ },
+ "VerticalToggleButtons": {
+ "rules": {
+ "aria-allowed-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-conditional-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-deprecated-role": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-hidden-focus": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-prohibited-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-required-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-roles": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "aria-valid-attr-value": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "button-name": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "nested-interactive": {
+ "status": "pass",
+ "tags": ["wcag2a"]
+ },
+ "target-size": {
+ "status": "pass",
+ "tags": ["wcag22aa"]
+ }
+ }
+ }
+}
diff --git a/packages/mui-material/src/ToggleButton/ToggleButton.test.js b/packages/mui-material/src/ToggleButton/ToggleButton.test.js
index b3c7915f282ca8..347bd9712c34f8 100644
--- a/packages/mui-material/src/ToggleButton/ToggleButton.test.js
+++ b/packages/mui-material/src/ToggleButton/ToggleButton.test.js
@@ -1,9 +1,16 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
-import { createRenderer, screen, isJsdom } from '@mui/internal-test-utils';
+import {
+ createRenderer,
+ screen,
+ isJsdom,
+ fireEvent,
+ focusVisible,
+ simulatePointerDevice,
+} from '@mui/internal-test-utils';
import ToggleButton, { toggleButtonClasses as classes } from '@mui/material/ToggleButton';
-import ButtonBase from '@mui/material/ButtonBase';
+import ButtonBase, { buttonBaseClasses } from '@mui/material/ButtonBase';
import describeConformance from '../../test/describeConformance';
describe('', () => {
@@ -150,6 +157,69 @@ describe('', () => {
});
});
+ describe('accessibility', () => {
+ it('reflects `selected` as `aria-pressed` (WCAG 4.1.2)', () => {
+ const { setProps } = render(
+
+ Bold
+ ,
+ );
+ const button = screen.getByRole('button');
+ expect(button).to.have.attribute('aria-pressed', 'false');
+
+ setProps({ selected: true });
+ expect(button).to.have.attribute('aria-pressed', 'true');
+ });
+
+ it('keeps the visible label within the accessible name (WCAG 2.5.3)', () => {
+ render(
+
+ Bold
+ ,
+ );
+
+ const button = screen.getByRole('button');
+ expect(button.getAttribute('aria-label')).to.contain(button.textContent);
+ });
+
+ it('activates on click, not on `mouseDown` alone (WCAG 2.5.2)', () => {
+ const handleChange = spy();
+ render(
+
+ Hello
+ ,
+ );
+ const button = screen.getByRole('button');
+
+ fireEvent.mouseDown(button);
+ expect(handleChange.callCount).to.equal(0);
+
+ button.click();
+ expect(handleChange.callCount).to.equal(1);
+ });
+
+ // `:focus-visible` is only reliable in a real browser, so this runs there (not jsdom).
+ it.skipIf(isJsdom())(
+ 'leaves no focus indicator (the ripple) under `disableRipple` (WCAG 2.4.7)',
+ () => {
+ render(
+
+ Hello
+ ,
+ );
+ const button = screen.getByRole('button');
+
+ simulatePointerDevice();
+ focusVisible(button);
+
+ // Keyboard focus is detected, but `disableRipple` leaves no visible indicator:
+ // the ripple is the only focus style the component provides.
+ expect(button).to.have.class(buttonBaseClasses.focusVisible);
+ expect(button.querySelector('.MuiTouchRipple-root')).to.equal(null);
+ },
+ );
+ });
+
describe.skipIf(!isJsdom())('server-side', () => {
it('should server-side render', () => {
const { container } = renderToString(Hello World);
diff --git a/packages/mui-material/src/ToggleButton/accessibility.md b/packages/mui-material/src/ToggleButton/accessibility.md
new file mode 100644
index 00000000000000..860bef82866e3d
--- /dev/null
+++ b/packages/mui-material/src/ToggleButton/accessibility.md
@@ -0,0 +1,361 @@
+# Toggle Button accessibility conformance
+
+Rated against WCAG 2.2 Level A and AA. See the [reports legend](../accessibility.md).
+
+| Result | Count |
+| :-------------------- | :---- |
+| ✅ Supports | 20 |
+| ⚠️ Partially Supports | 4 |
+| ❌ Does Not Support | 0 |
+| ➖ Not Applicable | 31 |
+| 🚩 Unverified | 14/24 |
+
+## Known gaps
+
+- ⚠️ **1.4.1 Use of Color.** For the color variants (`primary`, `error`, `info`, `success`), the selected and unselected labels are near-identical in grayscale lightness, so the pressed state is conveyed almost entirely by hue.
+- ⚠️ **1.4.3 Contrast (Minimum).** When selected, the `primary`, `error`, `info`, and `warning` labels (their `color.main` text over the tinted selected background) fall short of 4.5:1.
+- ⚠️ **1.4.11 Non-text Contrast.** The focus indicator is the ripple; `disableRipple`/`disableFocusRipple` remove it, leaving no focus indicator. The selected-state fill is untested for 3:1.
+- ⚠️ **2.4.7 Focus Visible.** `disableRipple`/`disableFocusRipple` remove the only keyboard focus indicator; the Toggle Button has no box-shadow fallback.
+
+## Success criteria
+
+### 🔍 Manual
+
+#### 1.3.2 Meaningful Sequence · A
+
+`🚩 Unverified` · `✅ Supports` · `○ Author`
+
+- The Toggle Button is one control. Its children render in DOM order; a decorative MUI icon is `aria-hidden`, leaving the label (text or `aria-label`) as the exposed content.
+- Order carries meaning only across several toggles, which the surrounding layout (often a `ToggleButtonGroup`) sets. Confirm that reading order matches the visual order.
+
+**Manual testing steps**
+
+1. Open a demo with a row of toggles (`ToggleButtons`).
+2. Press Tab repeatedly and note the order the toggles receive focus.
+3. Compare that order to the visual left-to-right order.
+
+**Pass:** focus order matches the visual order. Watch for layouts that reorder toggles visually without changing the DOM.
+
+#### 1.3.3 Sensory Characteristics · A
+
+`🚩 Unverified` · `✅ Supports` · `○ Author`
+
+- A toggle with text or an `aria-label` can be identified by name, not only by shape, color, or position.
+- Instructions in the surrounding content must not rely on color, shape, or position alone (for example, "the highlighted button").
+
+**Manual testing steps**
+
+1. Find any product copy that tells users to operate a toggle.
+2. Check that it names the toggle by its label, not only by color, shape, size, or position.
+
+**Pass:** no instruction relies on "the highlighted one" or "the button on the left" without naming it.
+
+#### 1.4.4 Resize Text · AA
+
+`🚩 Unverified` · `✅ Supports` · `◐ Shared`
+
+- Typography is set in rem and em, so the label and toggle scale with browser zoom or font size rather than staying pixel-fixed.
+- A fixed-pixel container in the surrounding layout could clip at 200%.
+
+**Manual testing steps**
+
+1. Open the Toggle Button demos and set browser zoom to 200% (Ctrl or Cmd and +).
+2. Confirm labels are fully visible and toggles still work.
+
+**Pass:** nothing is clipped or cut off at 200%.
+
+#### 1.4.5 Images of Text · AA
+
+`🚩 Unverified` · `✅ Supports` · `○ Author`
+
+- A text label is live CSS text; the component never renders it as an image.
+- This fails only if an image of text, or an icon font spelling words, is passed as the children. Use real text or an icon with an `aria-label`.
+
+**Manual testing steps**
+
+1. Open a toggle with a text label and try to select the text with the mouse, or zoom in.
+2. Confirm it behaves like real text (selectable, stays crisp), not a picture.
+
+**Pass:** labels are live text. No toggle uses an image of text.
+
+#### 1.4.10 Reflow · AA
+
+`🚩 Unverified` · `✅ Supports` · `◐ Shared`
+
+- A toggle sizes to its content, so it reflows on its own. Horizontal overflow at 320 CSS pixels comes from the surrounding layout, such as a wide `ToggleButtonGroup` that does not wrap.
+
+**Manual testing steps**
+
+1. Open the Toggle Button demos and set the window, or the DevTools device toolbar, to 320 CSS pixels wide.
+2. Confirm there is no sideways scrolling and all toggles are reachable.
+
+**Pass:** content reflows with no horizontal scroll.
+
+#### 2.4.11 Focus Not Obscured (Minimum) · AA
+
+`🚩 Unverified` · `✅ Supports` · `○ Author`
+
+- The Toggle Button is an ordinary focusable element and never places itself behind other content. Obscuring comes from sticky headers, banners, or overlays in the surrounding layout.
+- Confirm by moving focus to a toggle beneath any sticky or overlay content at several scroll positions. At least part of it must stay visible.
+
+**Manual testing steps**
+
+1. In a page that has a sticky header, footer, or banner, press Tab to move focus onto a toggle near it.
+2. Scroll so the toggle sits under the sticky element, then Tab to it again.
+
+**Pass:** at least part of the focused toggle stays visible, never fully covered.
+
+#### 3.2.4 Consistent Identification · AA
+
+`🚩 Unverified` · `✅ Supports` · `○ Author`
+
+- The component produces one stable accessible name per set of props, the precondition for consistent identification.
+- Consistency is a cross-page property. Confirm that toggles with the same function share a label and icon, and that one label is not reused for different functions.
+
+**Manual testing steps**
+
+1. List the toggles that do the same job across the product (for example, every "Bold").
+2. Compare their labels and icons.
+
+**Pass:** the same job uses the same label and icon, and no single label is reused for different jobs.
+
+### 🔁 Hybrid
+
+#### 1.1.1 Non-text Content · A
+
+`✅ Supports` · `◐ Shared`
+
+- A MUI `SvgIcon` child defaults to `aria-hidden` + `focusable="false"`, so it does not contribute to the name; the name comes from the children text or an `aria-label`. axe-core confirms a non-empty name (`button-name` on the native-button demos, `aria-command-name` on the non-native host).
+- An icon-only toggle has no accessible name unless the author sets an `aria-label`, as every icon toggle in the demos does. Whether the name conveys meaning needs an assistive-technology review.
+
+**Manual testing steps**
+
+1. Open `ToggleButtonA11ySemanticStates` with a screen reader running (NVDA with Chrome, or VoiceOver with Safari).
+2. Tab to each toggle and confirm it announces the visible label or `aria-label`, and does not read out the icon.
+3. For any icon-only toggle in the product, confirm it has an `aria-label` describing the action.
+
+**Pass:** every toggle's announced name matches its purpose, and decorative icons are silent.
+
+#### 1.3.1 Info and Relationships · A
+
+`✅ Supports` · `◐ Shared`
+
+- The component sets the role (`button`, or `role="button"` for a non-native host), the pressed state through `aria-pressed` (`ToggleButton.js`), and the disabled state. axe-core's ARIA rules (`aria-allowed-attr`, `aria-valid-attr-value`, and `aria-roles` where a non-default role is present) pass across the demos.
+- Grouping relationships (a `role="group"` and its label) belong to `ToggleButtonGroup`, not the single toggle. Whether a runtime state change is announced needs an assistive-technology review.
+
+**Manual testing steps**
+
+1. Open `ToggleButtonA11ySemanticStates` and `ToggleButtonA11yNonNative` with a screen reader running.
+2. Confirm the plain toggle announces "button", the selected one announces its pressed state, the disabled one announces its disabled state, and the custom one announces "button".
+
+**Pass:** role and state match the visual presentation for every variant.
+
+#### 1.4.1 Use of Color · A
+
+`🚩 Unverified` · `⚠️ Partially Supports` · `● Component`
+
+- A toggle's purpose comes from its label or icon, not color, so the control itself is not identified by color alone.
+- The pressed state is the gap. For `standard`, selecting darkens the label from `action.active` to `text.primary`, a clear lightness change. For the color variants the selected label switches to `color.main`, which is almost the same lightness as the unselected `action.active` (in grayscale `primary` differs by about 0.0003, `error` by 0.017), and the only other cue is a roughly 1.1:1 fill, so a `primary`/`error`/`info`/`success` toggle's pressed state is conveyed almost entirely by hue.
+
+**Manual testing steps**
+
+1. Open `ColorToggleButton` or `ToggleButtonA11yColorMatrix`.
+2. Turn on a grayscale view (in Chrome DevTools: Rendering tab, Emulate vision deficiencies, Achromatopsia).
+3. Confirm each toggle can still be told apart, and that a selected toggle is still distinguishable from an unselected one.
+
+**Pass:** the control's meaning survives without color. The `standard` selected state stays distinguishable; the color variants do not, since their pressed cue is hue-dominant.
+
+#### 1.4.3 Contrast (Minimum) · AA
+
+`⚠️ Partially Supports` · `● Component`
+
+- When selected, a toggle uses `color.main` as the label color over a `color.main`-tinted background (`ToggleButton.js`). The `primary`, `error`, `info`, and `warning` labels fall short of 4.5:1; `ToggleButtonA11yColorMatrix` records the `color-contrast` failure in [`toggle-button.a11y.json`](../../../../docs/data/material/components/toggle-button/toggle-button.a11y.json) (rule-level pass/fail only). The unselected label (`action.active`) and the `standard` selected label pass.
+- axe-core `color-contrast` checks the resting state. The `:hover` colors and custom palettes need a visual check. Disabled toggles are exempt.
+
+**Manual testing steps**
+
+1. Open `ToggleButtonA11yColorMatrix`. With a contrast checker (the color picker in browser DevTools shows a ratio), check each selected label against its background.
+2. Repeat with the pointer hovering, since `:hover` changes the background.
+3. Check any custom theme colors the product uses.
+
+**Pass:** at least 4.5:1, or 3:1 for large text, in the resting and hover states. Disabled is exempt; selected `primary`, `error`, `info`, and `warning` are the known failures.
+
+#### 1.4.11 Non-text Contrast · AA
+
+`🚩 Unverified` · `⚠️ Partially Supports` · `● Component`
+
+- The label or icon identifies the control, so WCAG does not require the `divider` border to reach 3:1. The pieces that do need 3:1 are the focus indicator and the visual cue that marks the pressed state.
+- The focus indicator is the ripple; `disableRipple`/`disableFocusRipple` remove it, so it can be absent entirely. The selected-state fill (`action.selectedOpacity`, 0.08) is faint and untested against the unselected state. Disabled toggles are exempt.
+
+**Manual testing steps**
+
+1. Open `ToggleButtons`. Press Tab to a toggle so its focus indicator shows, and measure the indicator against the colors next to it.
+2. Measure a selected toggle's fill (and any meaningful icon) against the unselected state and the background.
+3. Set `disableRipple` or `disableFocusRipple` on a toggle (no demo ships these) and repeat: the focus indicator should be gone.
+
+**Pass:** the focus indicator and any meaningful icon are each at least 3:1, and the pressed state stays identifiable. Disabled parts are exempt.
+
+#### 1.4.12 Text Spacing · AA
+
+`🚩 Unverified` · `✅ Supports` · `◐ Shared`
+
+- A text label wraps when `white-space: normal` is set and the toggle height comes from padding, not a fixed height, so the WCAG text-spacing values grow the toggle without clipping.
+- No axe-core rule covers this (`avoid-inline-spacing` only inspects inline `style`, but `sx` compiles to a class); a `ToggleButtonA11yTextSpacing` screenshot guards the rendered spacing layout against regressions. Inject the spacing values through a stylesheet to check for clipping.
+
+**Manual testing steps**
+
+1. Open `ToggleButtonA11yTextSpacing`.
+2. Apply the WCAG text-spacing values. The quickest way is to run this in the DevTools console: `document.head.insertAdjacentHTML('beforeend','')`.
+3. Look for cut-off, clipped, or overlapping label text.
+
+**Pass:** all label text stays visible and the toggle still works.
+
+#### 2.4.6 Headings and Labels · AA
+
+`✅ Supports` · `◐ Shared`
+
+- The accessible name serves as the control's label. axe-core confirms it is present (`button-name` on the native-button demos, `aria-command-name` on the non-native host).
+- Whether the label describes the action (a clear `aria-label` against a vague one) is a content decision, confirmed with a manual review.
+
+**Manual testing steps**
+
+1. Read each toggle's label, or its `aria-label`, out of context.
+2. Ask whether it says what the toggle does.
+
+**Pass:** every label describes its action ("Bold", not "B").
+
+#### 2.4.7 Focus Visible · AA
+
+`⚠️ Partially Supports` · `● Component`
+
+- Keyboard focus shows the `.Mui-focusVisible` ripple (suppressed for mouse). The component sets no other focus style.
+- `disableRipple` removes every ripple and `disableFocusRipple` removes the focus ripple, so either prop leaves the toggle with no visible focus indicator (the `disableRipple` prop documents this). [`ToggleButton.test.js`](./ToggleButton.test.js) confirms a keyboard-focused toggle renders no ripple under `disableRipple`.
+
+**Manual testing steps**
+
+1. Open `ToggleButtons` and `StandaloneToggleButton`. Press Tab to move to each toggle.
+2. Confirm a clear focus indicator appears, and that it looks different from the selected style.
+3. Click a toggle with the mouse and confirm the indicator does not appear (it is keyboard-only).
+4. Set `disableRipple` or `disableFocusRipple` on a toggle (no demo ships these) and Tab to it.
+
+**Pass:** every keyboard-focused toggle shows a visible indicator, including under `disableRipple` and `disableFocusRipple`.
+
+#### 2.5.3 Label in Name · A
+
+`✅ Supports` · `◐ Shared`
+
+- When a toggle has a visible text label, that text is the accessible name (the children become the name). An icon-only toggle has no visible text, so its `aria-label` is the name. [`ToggleButton.test.js`](./ToggleButton.test.js) confirms the accessible name contains the visible label.
+- An `aria-label` that omits or reorders the visible words breaks this. Compare the visible text to the computed name.
+
+**Manual testing steps**
+
+1. Open the accessibility tree (in Chrome DevTools: Elements panel, Accessibility tab) and select a toggle with a visible text label.
+2. Compare its computed Name to the words shown on screen.
+3. Optional: with Voice Control (macOS) or Dragon, say "click [label]" and confirm it activates.
+
+**Pass:** the visible text appears in the accessible name. An `aria-label` that drops or reorders the visible words fails.
+
+#### 4.1.2 Name, Role, Value · A
+
+`✅ Supports` · `◐ Shared`
+
+- A native `