diff --git a/docs/pages/material-ui/api/step-button.json b/docs/pages/material-ui/api/step-button.json
index c17afb77ca85a7..d9600c36f2505c 100644
--- a/docs/pages/material-ui/api/step-button.json
+++ b/docs/pages/material-ui/api/step-button.json
@@ -2,6 +2,13 @@
"props": {
"children": { "type": { "name": "node" } },
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
+ "getAriaLabel": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(index: number, totalSteps: number) => string",
+ "describedArgs": ["index", "totalSteps"]
+ }
+ },
"icon": { "type": { "name": "node" } },
"optional": { "type": { "name": "node" } },
"sx": {
diff --git a/docs/pages/material-ui/api/step.json b/docs/pages/material-ui/api/step.json
index cdab6d8edc0bac..22ecd06ea6cfa5 100644
--- a/docs/pages/material-ui/api/step.json
+++ b/docs/pages/material-ui/api/step.json
@@ -7,6 +7,13 @@
"component": { "type": { "name": "elementType" } },
"disabled": { "type": { "name": "bool" } },
"expanded": { "type": { "name": "bool" }, "default": "false" },
+ "getAriaLabel": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(index: number, totalSteps: number) => string",
+ "describedArgs": ["index", "totalSteps"]
+ }
+ },
"index": { "type": { "name": "custom", "description": "integer" } },
"last": { "type": { "name": "bool" } },
"sx": {
diff --git a/docs/pages/material-ui/api/stepper.json b/docs/pages/material-ui/api/stepper.json
index 2f33724a4fb497..390140656d3aaf 100644
--- a/docs/pages/material-ui/api/stepper.json
+++ b/docs/pages/material-ui/api/stepper.json
@@ -6,6 +6,13 @@
"classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
"component": { "type": { "name": "elementType" } },
"connector": { "type": { "name": "element" }, "default": "" },
+ "getAriaLabel": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(totalSteps: number) => string",
+ "describedArgs": ["totalSteps"]
+ }
+ },
"nonLinear": { "type": { "name": "bool" }, "default": "false" },
"orientation": {
"type": { "name": "enum", "description": "'horizontal'
| 'vertical'" },
diff --git a/docs/translations/api-docs/step-button/step-button.json b/docs/translations/api-docs/step-button/step-button.json
index 7c794c6a98f746..d2a6cd8c676346 100644
--- a/docs/translations/api-docs/step-button/step-button.json
+++ b/docs/translations/api-docs/step-button/step-button.json
@@ -5,6 +5,13 @@
"description": "Can be a StepLabel or a node to place inside StepLabel as children."
},
"classes": { "description": "Override or extend the styles applied to the component." },
+ "getAriaLabel": {
+ "description": "Accepts a function which returns a string value that provides a user-friendly name for the step button. This is important for screen reader users.",
+ "typeDescriptions": {
+ "index": { "name": "index", "description": "The step's index." },
+ "totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
+ }
+ },
"icon": { "description": "The icon displayed by the step label." },
"optional": { "description": "The optional node to display." },
"sx": {
diff --git a/docs/translations/api-docs/step/step.json b/docs/translations/api-docs/step/step.json
index 865c4708542f57..32ee2782cfdbe6 100644
--- a/docs/translations/api-docs/step/step.json
+++ b/docs/translations/api-docs/step/step.json
@@ -14,6 +14,13 @@
"description": "If true, the step is disabled, will also disable the button if StepButton is a child of Step. Is passed to child components."
},
"expanded": { "description": "Expand the step." },
+ "getAriaLabel": {
+ "description": "Accepts a function which returns a string value that provides a user-friendly name for the step. This is important for screen reader users.",
+ "typeDescriptions": {
+ "index": { "name": "index", "description": "The step's index." },
+ "totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
+ }
+ },
"index": {
"description": "The position of the step. The prop defaults to the value inherited from the parent Stepper component."
},
diff --git a/docs/translations/api-docs/stepper/stepper.json b/docs/translations/api-docs/stepper/stepper.json
index 1cad97543c78f7..c447fe71c0fa7a 100644
--- a/docs/translations/api-docs/stepper/stepper.json
+++ b/docs/translations/api-docs/stepper/stepper.json
@@ -13,6 +13,12 @@
"description": "The component used for the root node. Either a string to use a HTML element or a component."
},
"connector": { "description": "An element to be placed between each step." },
+ "getAriaLabel": {
+ "description": "Accepts a function which returns a string value that provides a user-friendly name for the stepper navigation. This is important for screen reader users when the stepper contains interactive steps.",
+ "typeDescriptions": {
+ "totalSteps": { "name": "totalSteps", "description": "The total number of steps." }
+ }
+ },
"nonLinear": {
"description": "If set the Stepper will not assist in controlling steps for linear flow."
},
diff --git a/packages/mui-material/src/Step/Step.d.ts b/packages/mui-material/src/Step/Step.d.ts
index ccb6f1044ff242..f0a97ca3c5237a 100644
--- a/packages/mui-material/src/Step/Step.d.ts
+++ b/packages/mui-material/src/Step/Step.d.ts
@@ -31,6 +31,14 @@ export interface StepOwnProps {
* @default false
*/
expanded?: boolean;
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the step.
+ * This is important for screen reader users.
+ * @param {number} index The step's index.
+ * @param {number} totalSteps The total number of steps.
+ * @returns {string}
+ */
+ getAriaLabel?: (index: number, totalSteps: number) => string;
/**
* The position of the step.
* The prop defaults to the value inherited from the parent Stepper component.
diff --git a/packages/mui-material/src/Step/Step.js b/packages/mui-material/src/Step/Step.js
index 048efdbb8a03a4..2d523d66bd362f 100644
--- a/packages/mui-material/src/Step/Step.js
+++ b/packages/mui-material/src/Step/Step.js
@@ -62,13 +62,20 @@ const Step = React.forwardRef(function Step(inProps, ref) {
completed: completedProp,
disabled: disabledProp,
expanded = false,
+ getAriaLabel,
index,
last,
...other
} = props;
- const { activeStep, connector, alternativeLabel, orientation, nonLinear } =
- React.useContext(StepperContext);
+ const {
+ activeStep,
+ connector,
+ alternativeLabel,
+ orientation,
+ nonLinear,
+ totalSteps = 0,
+ } = React.useContext(StepperContext);
let [active = false, completed = false, disabled = false] = [
activeProp,
@@ -85,8 +92,8 @@ const Step = React.forwardRef(function Step(inProps, ref) {
}
const contextValue = React.useMemo(
- () => ({ index, last, expanded, icon: index + 1, active, completed, disabled }),
- [index, last, expanded, active, completed, disabled],
+ () => ({ index, last, expanded, icon: index + 1, active, completed, disabled, totalSteps }),
+ [index, last, expanded, active, completed, disabled, totalSteps],
);
const ownerState = {
@@ -102,12 +109,17 @@ const Step = React.forwardRef(function Step(inProps, ref) {
const classes = useUtilityClasses(ownerState);
+ // Only add aria-label if user explicitly provides getAriaLabel
+ // Otherwise, rely on visually hidden text in StepLabel to avoid redundancy
+ const ariaLabel = getAriaLabel ? getAriaLabel(index, totalSteps) : undefined;
+
const newChildren = (
{connector && alternativeLabel && index !== 0 ? connector : null}
@@ -169,6 +181,14 @@ Step.propTypes /* remove-proptypes */ = {
* @default false
*/
expanded: PropTypes.bool,
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the step.
+ * This is important for screen reader users.
+ * @param {number} index The step's index.
+ * @param {number} totalSteps The total number of steps.
+ * @returns {string}
+ */
+ getAriaLabel: PropTypes.func,
/**
* The position of the step.
* The prop defaults to the value inherited from the parent Stepper component.
diff --git a/packages/mui-material/src/Step/Step.test.js b/packages/mui-material/src/Step/Step.test.js
index d9e2c5a098c6bc..8e99a6e9836314 100644
--- a/packages/mui-material/src/Step/Step.test.js
+++ b/packages/mui-material/src/Step/Step.test.js
@@ -121,4 +121,45 @@ describe('', () => {
expect(stepLabels[1]).to.have.class(stepLabelClasses.disabled);
});
});
+
+ describe('accessibility', () => {
+ it('should not have aria-label by default (relies on StepLabel visually hidden text)', () => {
+ const { container } = render(
+
+
+ Step 1
+
+
+ Step 2
+
+
+ Step 3
+
+ ,
+ );
+
+ const steps = container.querySelectorAll(`.${classes.root}`);
+ // No aria-label by default to avoid redundancy with visually hidden text
+ expect(steps[0]).not.to.have.attribute('aria-label');
+ expect(steps[1]).not.to.have.attribute('aria-label');
+ expect(steps[2]).not.to.have.attribute('aria-label');
+ });
+
+ it('should use custom getAriaLabel when provided', () => {
+ const { container } = render(
+
+ `Item ${index + 1} of ${totalSteps}`}>
+ First
+
+ `Item ${index + 1} of ${totalSteps}`}>
+ Second
+
+ ,
+ );
+
+ const steps = container.querySelectorAll(`.${classes.root}`);
+ expect(steps[0]).to.have.attribute('aria-label', 'Item 1 of 2');
+ expect(steps[1]).to.have.attribute('aria-label', 'Item 2 of 2');
+ });
+ });
});
diff --git a/packages/mui-material/src/Step/StepContext.ts b/packages/mui-material/src/Step/StepContext.ts
index a48e883b22fd62..114b1885f5b278 100644
--- a/packages/mui-material/src/Step/StepContext.ts
+++ b/packages/mui-material/src/Step/StepContext.ts
@@ -9,6 +9,7 @@ export interface StepContextType {
active: boolean;
completed: boolean;
disabled: boolean;
+ totalSteps: number;
}
/**
diff --git a/packages/mui-material/src/StepButton/StepButton.d.ts b/packages/mui-material/src/StepButton/StepButton.d.ts
index 0dae1f9975af1b..fb2559509ce63e 100644
--- a/packages/mui-material/src/StepButton/StepButton.d.ts
+++ b/packages/mui-material/src/StepButton/StepButton.d.ts
@@ -14,6 +14,14 @@ export interface StepButtonOwnProps {
* Override or extend the styles applied to the component.
*/
classes?: Partial;
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the step button.
+ * This is important for screen reader users.
+ * @param {number} index The step's index.
+ * @param {number} totalSteps The total number of steps.
+ * @returns {string}
+ */
+ getAriaLabel?: (index: number, totalSteps: number) => string;
/**
* The icon displayed by the step label.
*/
diff --git a/packages/mui-material/src/StepButton/StepButton.js b/packages/mui-material/src/StepButton/StepButton.js
index 86b75c33b6a9f7..70f8a177009981 100644
--- a/packages/mui-material/src/StepButton/StepButton.js
+++ b/packages/mui-material/src/StepButton/StepButton.js
@@ -57,9 +57,10 @@ const StepButtonRoot = styled(ButtonBase, {
const StepButton = React.forwardRef(function StepButton(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiStepButton' });
- const { children, className, icon, optional, ...other } = props;
+ const { children, className, getAriaLabel, icon, optional, ...other } = props;
- const { disabled, active } = React.useContext(StepContext);
+ const stepContext = React.useContext(StepContext);
+ const { disabled, active, index, totalSteps = 0 } = stepContext;
const { orientation } = React.useContext(StepperContext);
const ownerState = { ...props, orientation };
@@ -77,6 +78,14 @@ const StepButton = React.forwardRef(function StepButton(inProps, ref) {
{children}
);
+ // Add aria-label with step position
+ let ariaLabel;
+ if (getAriaLabel) {
+ ariaLabel = getAriaLabel(index, totalSteps);
+ } else if (totalSteps > 0 && index !== undefined) {
+ ariaLabel = `Step ${index + 1} of ${totalSteps}`;
+ }
+
return (
{child}
@@ -110,6 +120,14 @@ StepButton.propTypes /* remove-proptypes */ = {
* @ignore
*/
className: PropTypes.string,
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the step button.
+ * This is important for screen reader users.
+ * @param {number} index The step's index.
+ * @param {number} totalSteps The total number of steps.
+ * @returns {string}
+ */
+ getAriaLabel: PropTypes.func,
/**
* The icon displayed by the step label.
*/
@@ -128,4 +146,6 @@ StepButton.propTypes /* remove-proptypes */ = {
]),
};
+StepButton.muiName = 'StepButton';
+
export default StepButton;
diff --git a/packages/mui-material/src/StepButton/StepButton.test.js b/packages/mui-material/src/StepButton/StepButton.test.js
index 71d9e8b29af0a5..d6c6f15fc98eeb 100644
--- a/packages/mui-material/src/StepButton/StepButton.test.js
+++ b/packages/mui-material/src/StepButton/StepButton.test.js
@@ -3,6 +3,7 @@ import { spy } from 'sinon';
import { createRenderer, screen, fireEvent, supportsTouch } from '@mui/internal-test-utils';
import StepButton, { stepButtonClasses as classes } from '@mui/material/StepButton';
import Step from '@mui/material/Step';
+import Stepper from '@mui/material/Stepper';
import StepLabel, { stepLabelClasses } from '@mui/material/StepLabel';
import ButtonBase from '@mui/material/ButtonBase';
import describeConformance from '../../test/describeConformance';
@@ -143,4 +144,69 @@ describe('', () => {
expect(screen.getByRole('button')).not.to.equal(null);
});
+
+ describe('accessibility', () => {
+ it('should have aria-label with step position', () => {
+ render(
+
+
+ Step 1
+
+
+ Step 2
+
+
+ Step 3
+
+ ,
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons[0]).to.have.attribute('aria-label', 'Step 1 of 3');
+ expect(buttons[1]).to.have.attribute('aria-label', 'Step 2 of 3');
+ expect(buttons[2]).to.have.attribute('aria-label', 'Step 3 of 3');
+ });
+
+ it('should use custom getAriaLabel', () => {
+ render(
+
+
+ `Go to step ${index + 1} of ${totalSteps}`}
+ >
+ First
+
+
+
+ `Go to step ${index + 1} of ${totalSteps}`}
+ >
+ Second
+
+
+ ,
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons[0]).to.have.attribute('aria-label', 'Go to step 1 of 2');
+ expect(buttons[1]).to.have.attribute('aria-label', 'Go to step 2 of 2');
+ });
+
+ it('should have aria-current="step" on active button', () => {
+ render(
+
+
+ Step 1
+
+
+ Step 2
+
+ ,
+ );
+
+ const buttons = screen.getAllByRole('button');
+ expect(buttons[0]).not.to.have.attribute('aria-current');
+ expect(buttons[1]).to.have.attribute('aria-current', 'step');
+ });
+ });
});
diff --git a/packages/mui-material/src/Stepper/Stepper.d.ts b/packages/mui-material/src/Stepper/Stepper.d.ts
index 5fc6837acaf0b0..2f783bff2f7843 100644
--- a/packages/mui-material/src/Stepper/Stepper.d.ts
+++ b/packages/mui-material/src/Stepper/Stepper.d.ts
@@ -20,6 +20,13 @@ export interface StepperOwnProps extends Pick string;
/**
* Two or more `` components.
*/
diff --git a/packages/mui-material/src/Stepper/Stepper.js b/packages/mui-material/src/Stepper/Stepper.js
index d80c29df95ede0..d9814e42ca92a4 100644
--- a/packages/mui-material/src/Stepper/Stepper.js
+++ b/packages/mui-material/src/Stepper/Stepper.js
@@ -63,6 +63,7 @@ const Stepper = React.forwardRef(function Stepper(inProps, ref) {
const {
activeStep = 0,
alternativeLabel = false,
+ getAriaLabel,
children,
className,
component = 'div',
@@ -83,6 +84,19 @@ const Stepper = React.forwardRef(function Stepper(inProps, ref) {
const classes = useUtilityClasses(ownerState);
const childrenArray = React.Children.toArray(children).filter(Boolean);
+ const totalSteps = childrenArray.length;
+
+ // Detect if stepper contains interactive steps (StepButton)
+ const hasInteractiveSteps = React.useMemo(() => {
+ return childrenArray.some((step) => {
+ const child = step.props?.children;
+ if (!child) {
+ return false;
+ }
+ return React.Children.toArray(child).some((c) => c?.type?.muiName === 'StepButton');
+ });
+ }, [childrenArray]);
+
const steps = childrenArray.map((step, index) => {
return React.cloneElement(step, {
index,
@@ -91,10 +105,18 @@ const Stepper = React.forwardRef(function Stepper(inProps, ref) {
});
});
const contextValue = React.useMemo(
- () => ({ activeStep, alternativeLabel, connector, nonLinear, orientation }),
- [activeStep, alternativeLabel, connector, nonLinear, orientation],
+ () => ({ activeStep, alternativeLabel, connector, nonLinear, orientation, totalSteps }),
+ [activeStep, alternativeLabel, connector, nonLinear, orientation, totalSteps],
);
+ // Add navigation attributes for interactive steppers
+ const navigationProps = hasInteractiveSteps
+ ? {
+ role: 'navigation',
+ 'aria-label': getAriaLabel ? getAriaLabel(totalSteps) : `Stepper with ${totalSteps} steps`,
+ }
+ : {};
+
return (
{steps}
@@ -149,6 +172,13 @@ Stepper.propTypes /* remove-proptypes */ = {
* @default
*/
connector: PropTypes.element,
+ /**
+ * Accepts a function which returns a string value that provides a user-friendly name for the stepper navigation.
+ * This is important for screen reader users when the stepper contains interactive steps.
+ * @param {number} totalSteps The total number of steps.
+ * @returns {string}
+ */
+ getAriaLabel: PropTypes.func,
/**
* If set the `Stepper` will not assist in controlling steps for linear flow.
* @default false
diff --git a/packages/mui-material/src/Stepper/Stepper.test.tsx b/packages/mui-material/src/Stepper/Stepper.test.tsx
index 4b80013ca2a22c..d87742d2b68f4c 100644
--- a/packages/mui-material/src/Stepper/Stepper.test.tsx
+++ b/packages/mui-material/src/Stepper/Stepper.test.tsx
@@ -1,6 +1,7 @@
import { expect } from 'chai';
import { createRenderer, screen } from '@mui/internal-test-utils';
import Step, { StepProps, stepClasses } from '@mui/material/Step';
+import StepButton from '@mui/material/StepButton';
import StepLabel from '@mui/material/StepLabel';
import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector';
import StepContent, { stepContentClasses } from '@mui/material/StepContent';
@@ -270,4 +271,59 @@ describe('', () => {
const stepper = container.querySelector(`.${classes.root}`);
expect(stepper).to.have.class(classes.nonLinear);
});
+
+ describe('accessibility', () => {
+ it('should have navigation role and aria-label for interactive steppers', () => {
+ const { container } = render(
+
+
+ Step 1
+
+
+ Step 2
+
+
+ Step 3
+
+ ,
+ );
+
+ const stepper = container.querySelector(`.${classes.root}`);
+ expect(stepper).to.have.attribute('role', 'navigation');
+ expect(stepper).to.have.attribute('aria-label', 'Stepper with 3 steps');
+ });
+
+ it('should not have navigation role for non-interactive steppers', () => {
+ const { container } = render(
+
+
+ Step 1
+
+
+ Step 2
+
+ ,
+ );
+
+ const stepper = container.querySelector(`.${classes.root}`);
+ expect(stepper).not.to.have.attribute('role');
+ expect(stepper).not.to.have.attribute('aria-label');
+ });
+
+ it('should use custom getAriaLabel for interactive steppers', () => {
+ const { container } = render(
+ `Checkout with ${totalSteps} steps`}>
+
+ Cart
+
+
+ Payment
+
+ ,
+ );
+
+ const stepper = container.querySelector(`.${classes.root}`);
+ expect(stepper).to.have.attribute('aria-label', 'Checkout with 2 steps');
+ });
+ });
});
diff --git a/packages/mui-material/src/Stepper/StepperContext.ts b/packages/mui-material/src/Stepper/StepperContext.ts
index 1bdbb3608a9783..c6ea6d116d5e0c 100644
--- a/packages/mui-material/src/Stepper/StepperContext.ts
+++ b/packages/mui-material/src/Stepper/StepperContext.ts
@@ -7,6 +7,7 @@ export interface StepperContextType {
connector: React.ReactNode;
nonLinear: boolean;
orientation: 'horizontal' | 'vertical';
+ totalSteps: number;
}
/**