diff --git a/docs/data/material/components/menus/CheckboxMenu.js b/docs/data/material/components/menus/CheckboxMenu.js
new file mode 100644
index 00000000000000..82d65b01c9a002
--- /dev/null
+++ b/docs/data/material/components/menus/CheckboxMenu.js
@@ -0,0 +1,39 @@
+import * as React from 'react';
+import Paper from '@mui/material/Paper';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import Check from '@mui/icons-material/Check';
+
+const options = ['Show toolbar', 'Show sidebar', 'Show status bar'];
+
+export default function CheckboxMenu() {
+ const [checked, setChecked] = React.useState({
+ 'Show toolbar': true,
+ });
+
+ const handleToggle = (option) => () => {
+ setChecked((prev) => ({ ...prev, [option]: !prev[option] }));
+ };
+
+ return (
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
diff --git a/docs/data/material/components/menus/CheckboxMenu.tsx b/docs/data/material/components/menus/CheckboxMenu.tsx
new file mode 100644
index 00000000000000..b7518fb5e79c39
--- /dev/null
+++ b/docs/data/material/components/menus/CheckboxMenu.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react';
+import Paper from '@mui/material/Paper';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import Check from '@mui/icons-material/Check';
+
+const options = ['Show toolbar', 'Show sidebar', 'Show status bar'];
+
+export default function CheckboxMenu() {
+ const [checked, setChecked] = React.useState>({
+ 'Show toolbar': true,
+ });
+
+ const handleToggle = (option: string) => () => {
+ setChecked((prev) => ({ ...prev, [option]: !prev[option] }));
+ };
+
+ return (
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
diff --git a/docs/data/material/components/menus/RadioMenu.js b/docs/data/material/components/menus/RadioMenu.js
new file mode 100644
index 00000000000000..1455be200f3735
--- /dev/null
+++ b/docs/data/material/components/menus/RadioMenu.js
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import Paper from '@mui/material/Paper';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import RadioButtonChecked from '@mui/icons-material/RadioButtonChecked';
+import RadioButtonUnchecked from '@mui/icons-material/RadioButtonUnchecked';
+
+const options = ['Name', 'Date modified', 'Size'];
+
+export default function RadioMenu() {
+ const [selected, setSelected] = React.useState('Name');
+
+ return (
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
diff --git a/docs/data/material/components/menus/RadioMenu.tsx b/docs/data/material/components/menus/RadioMenu.tsx
new file mode 100644
index 00000000000000..1455be200f3735
--- /dev/null
+++ b/docs/data/material/components/menus/RadioMenu.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import Paper from '@mui/material/Paper';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemIcon from '@mui/material/ListItemIcon';
+import RadioButtonChecked from '@mui/icons-material/RadioButtonChecked';
+import RadioButtonUnchecked from '@mui/icons-material/RadioButtonUnchecked';
+
+const options = ['Name', 'Date modified', 'Size'];
+
+export default function RadioMenu() {
+ const [selected, setSelected] = React.useState('Name');
+
+ return (
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
diff --git a/docs/data/material/components/menus/menus.md b/docs/data/material/components/menus/menus.md
index 9ae8536ade0a43..0ae32bfd270d10 100644
--- a/docs/data/material/components/menus/menus.md
+++ b/docs/data/material/components/menus/menus.md
@@ -52,6 +52,17 @@ To use a selected menu item without impacting the initial focus, set the `varian
{{"demo": "SimpleListMenu.js"}}
+## Checkbox and radio menu items
+
+To build a menu of toggleable options, set each item's `role` to `menuitemcheckbox` for independent toggles, or `menuitemradio` for a single choice within a group.
+For these roles, the `selected` prop drives `aria-checked`, so assistive technologies announce the checked state.
+
+{{"demo": "CheckboxMenu.js", "bg": true}}
+
+For a single choice within a group, use `menuitemradio`:
+
+{{"demo": "RadioMenu.js", "bg": true}}
+
## Positioned menu
Because the `Menu` component uses the `Popover` component to position itself, you can use the same [positioning props](/material-ui/react-popover/#anchor-playground) to position it.
diff --git a/docs/translations/api-docs/menu-item/menu-item.json b/docs/translations/api-docs/menu-item/menu-item.json
index 777888417dec8f..6ce4ae16a19147 100644
--- a/docs/translations/api-docs/menu-item/menu-item.json
+++ b/docs/translations/api-docs/menu-item/menu-item.json
@@ -21,7 +21,9 @@
"focusVisibleClassName": {
"description": "This prop can help identify which element has keyboard focus. The class name will be applied when the element gains the focus through keyboard interaction. It's a polyfill for the CSS :focus-visible selector. The rationale for using this feature is explained here. A polyfill can be used to apply a focus-visible class to other components if needed."
},
- "selected": { "description": "If true, the component is selected." },
+ "selected": {
+ "description": "If true, the component is selected. For menuitemcheckbox and menuitemradio roles, this also drives aria-checked."
+ },
"sx": {
"description": "The system prop that allows defining system overrides as well as additional CSS styles."
}
diff --git a/packages/mui-material/src/MenuItem/MenuItem.d.ts b/packages/mui-material/src/MenuItem/MenuItem.d.ts
index 01ea591389ab78..90f51d0633c5af 100644
--- a/packages/mui-material/src/MenuItem/MenuItem.d.ts
+++ b/packages/mui-material/src/MenuItem/MenuItem.d.ts
@@ -39,6 +39,7 @@ export interface MenuItemOwnProps {
divider?: boolean | undefined;
/**
* If `true`, the component is selected.
+ * For `menuitemcheckbox` and `menuitemradio` roles, this also drives `aria-checked`.
* @default false
*/
selected?: boolean | undefined;
diff --git a/packages/mui-material/src/MenuItem/MenuItem.js b/packages/mui-material/src/MenuItem/MenuItem.js
index 68df44938b4404..1fc1ecbba1a95b 100644
--- a/packages/mui-material/src/MenuItem/MenuItem.js
+++ b/packages/mui-material/src/MenuItem/MenuItem.js
@@ -181,6 +181,11 @@ const MenuItem = React.forwardRef(function MenuItem(inProps, ref) {
...other
} = props;
+ // `menuitemcheckbox`/`menuitemradio` require `aria-checked`; derive it from `selected`
+ // (an omitted `selected` means unchecked). Other roles keep `selected` presentational.
+ const isCheckableRole = role === 'menuitemcheckbox' || role === 'menuitemradio';
+ const ariaChecked = isCheckableRole ? Boolean(props.selected) : undefined;
+
const focusSource = useSelectFocusSource();
const context = React.useContext(ListContext);
const childContext = React.useMemo(
@@ -250,6 +255,7 @@ const MenuItem = React.forwardRef(function MenuItem(inProps, ref) {
', () => {
expect(menuitem).to.have.class(classes.selected);
expect(menuitem).not.to.have.attribute('aria-selected');
+ expect(menuitem).not.to.have.attribute('aria-checked');
+ });
+
+ it('drives aria-checked from `selected` for `role="menuitemcheckbox"`', () => {
+ const { rerender } = render(
+
+
+ ,
+ );
+ const menuitem = screen.getByRole('menuitemcheckbox');
+
+ expect(menuitem).to.have.attribute('aria-checked', 'true');
+ expect(menuitem).not.to.have.attribute('aria-selected');
+
+ rerender(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('menuitemcheckbox')).to.have.attribute('aria-checked', 'false');
+ });
+
+ it('sets aria-checked="false" for a `menuitemcheckbox` when `selected` is omitted', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('menuitemcheckbox')).to.have.attribute('aria-checked', 'false');
+ });
+
+ it('drives aria-checked from `selected` for `role="menuitemradio"`', () => {
+ render(
+
+
+ ,
+ );
+ const menuitem = screen.getByRole('menuitemradio');
+
+ expect(menuitem).to.have.attribute('aria-checked', 'true');
+ expect(menuitem).not.to.have.attribute('aria-selected');
+ });
+
+ it('keeps `selected` presentational and omits aria-checked for non-checkable roles', () => {
+ render(
+
+
+ ,
+ );
+ const option = screen.getByRole('option');
+
+ expect(option).to.have.class(classes.selected);
+ expect(option).not.to.have.attribute('aria-checked');
});
it('can have a role of option', () => {