diff --git a/docs/data/api/menu-checkbox-item-indicator.json b/docs/data/api/menu-checkbox-item-indicator.json
new file mode 100644
index 0000000000..aaf1adec7d
--- /dev/null
+++ b/docs/data/api/menu-checkbox-item-indicator.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func
| string" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "true" },
+ "render": { "type": { "name": "union", "description": "element
| func" } }
+ },
+ "name": "MenuCheckboxItemIndicator",
+ "imports": [
+ "import * as Menu from '@base_ui/react/Menu';\nconst MenuCheckboxItemIndicator = Menu.CheckboxItemIndicator;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "MenuCheckboxItemIndicator",
+ "forwardsRefTo": "HTMLSpanElement",
+ "filename": "/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.tsx",
+ "inheritance": null,
+ "demos": "
@@ -177,6 +179,34 @@ If you keep the state externally (and use the `checked` prop), this isn't requir
+## Checkbox items
+
+Menu items can act as checkboxes.
+
+
+
+
+ If you rely on the CheckboxItem to manage its state (e.g., you use the `defaultChecked` and `onCheckedChange` props), ensure that the item is not unmounted when its parent menu is closed.
+ Unmounting the component resets its state.
+
+To do this, add the `keepMounted` prop to the `Menu.Positioner` the checkbox item is in (and all parent positioners, in the case of a nested menu):
+
+```jsx
+
+
+
+
+ Item 1
+
+
+
+
+```
+
+If you keep the state externally (and use the `checked` prop), this isn't required.
+
+
+
## Nested menu
Menu items can open submenus.
diff --git a/docs/data/translations/api-docs/menu-checkbox-item-indicator/menu-checkbox-item-indicator.json b/docs/data/translations/api-docs/menu-checkbox-item-indicator/menu-checkbox-item-indicator.json
new file mode 100644
index 0000000000..b30e00bb16
--- /dev/null
+++ b/docs/data/translations/api-docs/menu-checkbox-item-indicator/menu-checkbox-item-indicator.json
@@ -0,0 +1,13 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "keepMounted": {
+ "description": "If true
, the component is mounted even if the checkbox is not checked."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/data/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json b/docs/data/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json
new file mode 100644
index 0000000000..a13fca2fee
--- /dev/null
+++ b/docs/data/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json
@@ -0,0 +1,15 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "closeOnClick": {
+ "description": "If true
, the menu will close when the menu item is clicked."
+ },
+ "disabled": { "description": "If true
, the menu item will be disabled." },
+ "id": { "description": "The id of the menu item." },
+ "label": {
+ "description": "A text representation of the menu item's content. Used for keyboard text navigation matching."
+ },
+ "onClick": { "description": "The click handler for the menu item." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/src/components/demo/DemoLoader.tsx b/docs/src/components/demo/DemoLoader.tsx
index d376808f9f..7bdad55fdc 100644
--- a/docs/src/components/demo/DemoLoader.tsx
+++ b/docs/src/components/demo/DemoLoader.tsx
@@ -24,6 +24,7 @@ export async function DemoLoader(props: DemoLoaderProps) {
return ;
} catch (error) {
if (process.env.NODE_ENV !== 'production') {
+ console.error(error);
return (
Unable to render the {demo} demo.
diff --git a/docs/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json b/docs/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json
new file mode 100644
index 0000000000..46541ccb2f
--- /dev/null
+++ b/docs/translations/api-docs/menu-checkbox-item/menu-checkbox-item.json
@@ -0,0 +1,15 @@
+{
+ "componentDescription": "An unstyled menu item to be used within a Menu.",
+ "propDescriptions": {
+ "closeOnClick": {
+ "description": "If true
, the menu will close when the menu item is clicked."
+ },
+ "disabled": { "description": "If true
, the menu item will be disabled." },
+ "id": { "description": "The id of the menu item." },
+ "label": {
+ "description": "A text representation of the menu item's content. Used for keyboard text navigation matching."
+ },
+ "onClick": { "description": "The click handler for the menu item." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-menu-checkbox-item/use-menu-checkbox-item.json b/docs/translations/api-docs/use-menu-checkbox-item/use-menu-checkbox-item.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-menu-checkbox-item/use-menu-checkbox-item.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.test.tsx b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.test.tsx
new file mode 100644
index 0000000000..fc48ebc875
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.test.tsx
@@ -0,0 +1,342 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { spy } from 'sinon';
+import userEvent from '@testing-library/user-event';
+import { fireEvent, act, waitFor } from '@mui/internal-test-utils';
+import { FloatingRootContext, FloatingTree } from '@floating-ui/react';
+import * as Menu from '@base_ui/react/Menu';
+import { MenuRootContext } from '@base_ui/react/Menu';
+import { describeConformance, createRenderer } from '../../../test';
+
+const testRootContext: MenuRootContext = {
+ floatingRootContext: {} as FloatingRootContext,
+ getPositionerProps: (p) => ({ ...p }),
+ getTriggerProps: (p) => ({ ...p }),
+ getItemProps: (p) => ({ ...p }),
+ parentContext: null,
+ nested: false,
+ triggerElement: null,
+ setTriggerElement: () => {},
+ setPositionerElement: () => {},
+ activeIndex: null,
+ disabled: false,
+ itemDomElements: { current: [] },
+ itemLabels: { current: [] },
+ open: true,
+ setOpen: () => {},
+ clickAndDragEnabled: false,
+ setClickAndDragEnabled: () => {},
+ popupRef: { current: null },
+ mounted: true,
+ transitionStatus: undefined,
+ typingRef: { current: false },
+};
+
+describe('
', () => {
+ const { render } = createRenderer();
+ const user = userEvent.setup();
+
+ describeConformance(, () => ({
+ render: (node) => {
+ return render(
+
+ {node}
+ ,
+ );
+ },
+ refInstanceof: window.HTMLDivElement,
+ }));
+
+ it('perf: does not rerender menu items unnecessarily', async function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
+ const renderItem1Spy = spy();
+ const renderItem2Spy = spy();
+ const renderItem3Spy = spy();
+ const renderItem4Spy = spy();
+
+ const LoggingRoot = React.forwardRef(function LoggingRoot(
+ props: any & { renderSpy: () => void },
+ ref: React.ForwardedRef,
+ ) {
+ const { renderSpy, ownerState, ...other } = props;
+ renderSpy();
+ return ;
+ });
+
+ const { getAllByRole } = await render(
+
+
+
+ } id="item-1">
+ 1
+
+ } id="item-2">
+ 2
+
+ } id="item-3">
+ 3
+
+ } id="item-4">
+ 4
+
+
+
+ ,
+ );
+
+ const menuItems = getAllByRole('menuitemcheckbox');
+ await act(() => {
+ menuItems[0].focus();
+ });
+
+ renderItem1Spy.resetHistory();
+ renderItem2Spy.resetHistory();
+ renderItem3Spy.resetHistory();
+ renderItem4Spy.resetHistory();
+
+ expect(renderItem1Spy.callCount).to.equal(0);
+
+ fireEvent.keyDown(menuItems[0], { key: 'ArrowDown' }); // highlights '2'
+
+ // React renders twice in strict mode, so we expect twice the number of spy calls
+ // Also, useButton's focusVisible polyfill causes an extra render when focus is gained/lost.
+
+ await waitFor(
+ () => {
+ expect(renderItem1Spy.callCount).to.equal(4); // '1' rerenders as it loses highlight
+ },
+ { timeout: 1000 },
+ );
+
+ await waitFor(
+ () => {
+ expect(renderItem2Spy.callCount).to.equal(4); // '2' rerenders as it receives highlight
+ },
+ { timeout: 1000 },
+ );
+
+ // neither the highlighted nor the selected state of these options changed,
+ // so they don't need to rerender:
+ expect(renderItem3Spy.callCount).to.equal(0);
+ expect(renderItem4Spy.callCount).to.equal(0);
+ });
+
+ describe('state management', () => {
+ (
+ [
+ [true, 'true', 'checked'],
+ [false, 'false', 'unchecked'],
+ ] as const
+ ).forEach(([checked, ariaChecked, dataState]) =>
+ it('adds the state and ARIA attributes when checked', async () => {
+ const { getByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ expect(item).to.have.attribute('aria-checked', ariaChecked);
+ expect(item).to.have.attribute('data-checkboxitem', dataState);
+ }),
+ );
+
+ it('toggles the checked state when clicked', async () => {
+ const { getByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ await user.click(item);
+
+ expect(item).to.have.attribute('aria-checked', 'true');
+ expect(item).to.have.attribute('data-checkboxitem', 'checked');
+
+ await user.click(item);
+
+ expect(item).to.have.attribute('aria-checked', 'false');
+ expect(item).to.have.attribute('data-checkboxitem', 'unchecked');
+ });
+
+ it(`toggles the checked state when Space is pressed`, async () => {
+ const { getByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await act(() => {
+ trigger.focus();
+ });
+ await user.keyboard('[ArrowDown]');
+ const item = getByRole('menuitemcheckbox');
+
+ await waitFor(() => {
+ expect(item).toHaveFocus();
+ });
+
+ await user.keyboard(`[Space]`);
+ expect(item).to.have.attribute('data-checkboxitem', 'checked');
+
+ await user.keyboard(`[Space]`);
+ expect(item).to.have.attribute('data-checkboxitem', 'unchecked');
+ });
+
+ it(`toggles the checked state and closes the menu when Enter is pressed`, async () => {
+ const { getByRole, queryByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await act(() => {
+ trigger.focus();
+ });
+
+ await user.keyboard('[ArrowDown]');
+ const item = getByRole('menuitemcheckbox');
+
+ await waitFor(() => {
+ expect(item).toHaveFocus();
+ });
+
+ await user.keyboard(`[Enter]`);
+
+ expect(queryByRole('menu', { hidden: false })).to.equal(null);
+ await user.click(trigger);
+ expect(item).to.have.attribute('data-checkboxitem', 'checked');
+ });
+
+ it('calls `onCheckedChange` when the item is clicked', async () => {
+ const onCheckedChange = spy();
+ const { getByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ await user.click(item);
+
+ expect(onCheckedChange.callCount).to.equal(1);
+ expect(onCheckedChange.lastCall.args[0]).to.equal(true);
+
+ await user.click(item);
+
+ expect(onCheckedChange.callCount).to.equal(2);
+ expect(onCheckedChange.lastCall.args[0]).to.equal(false);
+ });
+
+ it('keeps the state when closed and reopened', async () => {
+ const { getByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ await user.click(item);
+
+ await user.click(trigger);
+
+ await user.click(trigger);
+
+ const itemAfterReopen = getByRole('menuitemcheckbox');
+ expect(itemAfterReopen).to.have.attribute('aria-checked', 'true');
+ expect(itemAfterReopen).to.have.attribute('data-checkboxitem', 'checked');
+ });
+ });
+
+ describe('prop: closeOnClick', () => {
+ it('when `closeOnClick=true`, closes the menu when the item is clicked', async () => {
+ const { getByRole, queryByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ await user.click(item);
+
+ expect(queryByRole('menu')).to.equal(null);
+ });
+
+ it('does not close the menu when the item is clicked by default', async () => {
+ const { getByRole, queryByRole } = await render(
+
+ Open
+
+
+ Item
+
+
+ ,
+ );
+
+ const trigger = getByRole('button', { name: 'Open' });
+ await user.click(trigger);
+
+ const item = getByRole('menuitemcheckbox');
+ await user.click(item);
+
+ expect(queryByRole('menu')).not.to.equal(null);
+ });
+ });
+});
diff --git a/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.tsx b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.tsx
new file mode 100644
index 0000000000..6fa0f04570
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItem.tsx
@@ -0,0 +1,219 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingEvents, useFloatingTree, useListItem } from '@floating-ui/react';
+import { useMenuCheckboxItem } from './useMenuCheckboxItem';
+import { MenuCheckboxItemContext } from './MenuCheckboxItemContext';
+import { useMenuRootContext } from '../Root/MenuRootContext';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
+import { useId } from '../../utils/useId';
+import type { BaseUIComponentProps, GenericHTMLProps } from '../../utils/types';
+import { useForkRef } from '../../utils/useForkRef';
+
+const customStyleHookMapping: CustomStyleHookMapping = {
+ checked: (value: boolean) => ({ 'data-checkboxitem': value ? 'checked' : 'unchecked' }),
+};
+
+const InnerMenuCheckboxItem = React.memo(
+ React.forwardRef(function InnerMenuItem(
+ props: InnerMenuCheckboxItemProps,
+ forwardedRef: React.ForwardedRef,
+ ) {
+ const {
+ checked: checkedProp,
+ defaultChecked,
+ onCheckedChange,
+ className,
+ closeOnClick = false,
+ disabled = false,
+ highlighted,
+ id,
+ menuEvents,
+ propGetter,
+ render,
+ treatMouseupAsClick,
+ typingRef,
+ ...other
+ } = props;
+
+ const { getRootProps, checked } = useMenuCheckboxItem({
+ closeOnClick,
+ disabled,
+ highlighted,
+ id,
+ menuEvents,
+ ref: forwardedRef,
+ treatMouseupAsClick,
+ checked: checkedProp,
+ defaultChecked,
+ onCheckedChange,
+ typingRef,
+ });
+
+ const ownerState: MenuCheckboxItem.OwnerState = React.useMemo(
+ () => ({ disabled, highlighted, checked }),
+ [disabled, highlighted, checked],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ render: render || 'div',
+ className,
+ ownerState,
+ propGetter: (externalProps) => propGetter(getRootProps(externalProps)),
+ customStyleHookMapping,
+ extraProps: other,
+ });
+
+ return (
+
+ {renderElement()}
+
+ );
+ }),
+);
+/**
+ *
+ * Demos:
+ *
+ * - [Menu](https://base-ui.netlify.app/components/react-menu/)
+ *
+ * API:
+ *
+ * - [MenuCheckboxItem API](https://base-ui.netlify.app/components/react-menu/#api-reference-MenuCheckboxItem)
+ */
+const MenuCheckboxItem = React.forwardRef(function MenuCheckboxItem(
+ props: MenuCheckboxItem.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { id: idProp, label, ...other } = props;
+
+ const itemRef = React.useRef(null);
+ const listItem = useListItem({ label: label ?? itemRef.current?.innerText });
+ const mergedRef = useForkRef(forwardedRef, listItem.ref, itemRef);
+
+ const { getItemProps, activeIndex, clickAndDragEnabled, typingRef } = useMenuRootContext();
+ const id = useId(idProp);
+
+ const highlighted = listItem.index === activeIndex;
+ const { events: menuEvents } = useFloatingTree()!;
+
+ // This wrapper component is used as a performance optimization.
+ // MenuCheckboxItem reads the context and re-renders the actual MenuCheckboxItem
+ // only when it needs to.
+
+ return (
+
+ );
+});
+
+// ESLint reporting false positives here
+interface InnerMenuCheckboxItemProps extends MenuCheckboxItem.Props {
+ // eslint-disable-next-line react/no-unused-prop-types
+ highlighted: boolean;
+ // eslint-disable-next-line react/no-unused-prop-types
+ propGetter: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ // eslint-disable-next-line react/no-unused-prop-types
+ menuEvents: FloatingEvents;
+ // eslint-disable-next-line react/no-unused-prop-types
+ treatMouseupAsClick: boolean;
+ // eslint-disable-next-line react/no-unused-prop-types
+ typingRef: React.RefObject;
+}
+
+namespace MenuCheckboxItem {
+ export type OwnerState = {
+ disabled: boolean;
+ highlighted: boolean;
+ checked: boolean;
+ };
+
+ export interface Props extends BaseUIComponentProps<'div', OwnerState> {
+ checked?: boolean;
+ defaultChecked?: boolean;
+ onCheckedChange?: (checked: boolean, event: Event) => void;
+ children?: React.ReactNode;
+ /**
+ * The click handler for the menu item.
+ */
+ onClick?: React.MouseEventHandler;
+ /**
+ * If `true`, the menu item will be disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * A text representation of the menu item's content.
+ * Used for keyboard text navigation matching.
+ */
+ label?: string;
+ /**
+ * The id of the menu item.
+ */
+ id?: string;
+ /**
+ * If `true`, the menu will close when the menu item is clicked.
+ *
+ * @default true
+ */
+ closeOnClick?: boolean;
+ }
+}
+
+MenuCheckboxItem.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ checked: PropTypes.bool,
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * If `true`, the menu will close when the menu item is clicked.
+ *
+ * @default true
+ */
+ closeOnClick: PropTypes.bool,
+ /**
+ * @ignore
+ */
+ defaultChecked: PropTypes.bool,
+ /**
+ * If `true`, the menu item will be disabled.
+ * @default false
+ */
+ disabled: PropTypes.bool,
+ /**
+ * The id of the menu item.
+ */
+ id: PropTypes.string,
+ /**
+ * A text representation of the menu item's content.
+ * Used for keyboard text navigation matching.
+ */
+ label: PropTypes.string,
+ /**
+ * @ignore
+ */
+ onCheckedChange: PropTypes.func,
+ /**
+ * The click handler for the menu item.
+ */
+ onClick: PropTypes.func,
+} as any;
+
+export { MenuCheckboxItem };
diff --git a/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItemContext.ts b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItemContext.ts
new file mode 100644
index 0000000000..beab8db3f2
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItem/MenuCheckboxItemContext.ts
@@ -0,0 +1,20 @@
+import * as React from 'react';
+
+export interface MenuCheckboxItemContext {
+ checked: boolean;
+ highlighted: boolean;
+ disabled: boolean;
+}
+
+export const MenuCheckboxItemContext = React.createContext(
+ undefined,
+);
+
+export function useMenuCheckboxItemContext() {
+ const context = React.useContext(MenuCheckboxItemContext);
+ if (context === undefined) {
+ throw new Error('useMenuCheckboxItemContext must be used within a MenuCheckboxItemProvider');
+ }
+
+ return context;
+}
diff --git a/packages/mui-base/src/Menu/CheckboxItem/useMenuCheckboxItem.ts b/packages/mui-base/src/Menu/CheckboxItem/useMenuCheckboxItem.ts
new file mode 100644
index 0000000000..6b2dc30624
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItem/useMenuCheckboxItem.ts
@@ -0,0 +1,63 @@
+import * as React from 'react';
+import { useMenuItem } from '../Item/useMenuItem';
+import { useControlled } from '../../utils/useControlled';
+import { GenericHTMLProps } from '../../utils/types';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+
+/**
+ *
+ * API:
+ *
+ * - [useMenuCheckboxItem API](https://mui.com/base-ui/api/use-menu-checkbox-item/)
+ */
+export function useMenuCheckboxItem(
+ params: useMenuCheckboxItem.Parameters,
+): useMenuCheckboxItem.ReturnValue {
+ const { checked: checkedProp, defaultChecked, onCheckedChange, ...other } = params;
+
+ const [checked, setChecked] = useControlled({
+ controlled: checkedProp,
+ default: defaultChecked ?? false,
+ name: 'MenuCheckboxItem',
+ state: 'checked',
+ });
+
+ const { getRootProps: getMenuItemRootProps, ...menuItem } = useMenuItem(other);
+
+ const getRootProps = React.useCallback(
+ (externalProps?: GenericHTMLProps): GenericHTMLProps => {
+ return getMenuItemRootProps(
+ mergeReactProps(externalProps, {
+ role: 'menuitemcheckbox',
+ 'aria-checked': checked,
+ onClick: (event: React.MouseEvent) => {
+ setChecked((currentlyChecked) => !currentlyChecked);
+ onCheckedChange?.(!checked, event.nativeEvent);
+ },
+ }),
+ );
+ },
+ [checked, getMenuItemRootProps, onCheckedChange, setChecked],
+ );
+
+ return React.useMemo(
+ () => ({
+ ...menuItem,
+ getRootProps,
+ checked,
+ }),
+ [checked, getRootProps, menuItem],
+ );
+}
+
+export namespace useMenuCheckboxItem {
+ export interface Parameters extends useMenuItem.Parameters {
+ checked?: boolean;
+ defaultChecked?: boolean;
+ onCheckedChange?: (checked: boolean, event: Event) => void;
+ }
+
+ export interface ReturnValue extends useMenuItem.ReturnValue {
+ checked: boolean;
+ }
+}
diff --git a/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.test.tsx b/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.test.tsx
new file mode 100644
index 0000000000..dd0cfefae8
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.test.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import * as Menu from '@base_ui/react/Menu';
+import { createRenderer, describeConformance } from '#test-utils';
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ refInstanceof: window.HTMLSpanElement,
+ render(node) {
+ return render(
+
+
+
+ {node}
+
+
+ ,
+ );
+ },
+ }));
+});
diff --git a/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.tsx b/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.tsx
new file mode 100644
index 0000000000..5f72304f55
--- /dev/null
+++ b/packages/mui-base/src/Menu/CheckboxItemIndicator/MenuCheckboxItemIndicator.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { MenuCheckboxItem } from '../CheckboxItem/MenuCheckboxItem';
+import { useMenuCheckboxItemContext } from '../CheckboxItem/MenuCheckboxItemContext';
+import { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { BaseUIComponentProps } from '../../utils/types';
+
+const customStyleHookMapping: CustomStyleHookMapping = {
+ checked: (value: boolean) => ({ 'data-checkboxitem': value ? 'checked' : 'unchecked' }),
+};
+
+/**
+ *
+ * Demos:
+ *
+ * - [Menu](https://base-ui.netlify.app/components/react-menu/)
+ *
+ * API:
+ *
+ * - [MenuCheckboxItemIndicator API](https://base-ui.netlify.app/components/react-menu/#api-reference-MenuCheckboxItemIndicator)
+ */
+const MenuCheckboxItemIndicator = React.forwardRef(function MenuCheckboxItemIndicatorComponent(
+ props: MenuCheckboxItemIndicator.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, keepMounted = true, ...other } = props;
+
+ const ownerState = useMenuCheckboxItemContext();
+
+ const { renderElement } = useComponentRenderer({
+ render: render || 'span',
+ className,
+ ownerState,
+ customStyleHookMapping,
+ extraProps: other,
+ ref: forwardedRef,
+ });
+
+ if (!keepMounted && !ownerState.checked) {
+ return null;
+ }
+
+ return renderElement();
+});
+
+MenuCheckboxItemIndicator.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * If `true`, the component is mounted even if the checkbox is not checked.
+ *
+ * @default true
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+namespace MenuCheckboxItemIndicator {
+ export interface Props extends BaseUIComponentProps<'span', OwnerState> {
+ /**
+ * If `true`, the component is mounted even if the checkbox is not checked.
+ *
+ * @default true
+ */
+ keepMounted?: boolean;
+ }
+
+ export interface OwnerState {
+ checked: boolean;
+ disabled: boolean;
+ highlighted: boolean;
+ }
+}
+
+export { MenuCheckboxItemIndicator };
diff --git a/packages/mui-base/src/Menu/index.ts b/packages/mui-base/src/Menu/index.ts
index 11e63ca146..8d6fd338f0 100644
--- a/packages/mui-base/src/Menu/index.ts
+++ b/packages/mui-base/src/Menu/index.ts
@@ -1,6 +1,11 @@
export { MenuArrow as Arrow } from './Arrow/MenuArrow';
export { useMenuArrow } from './Arrow/useMenuArrow';
+export { MenuCheckboxItem as CheckboxItem } from './CheckboxItem/MenuCheckboxItem';
+export { useMenuCheckboxItem } from './CheckboxItem/useMenuCheckboxItem';
+
+export { MenuCheckboxItemIndicator as CheckboxItemIndicator } from './CheckboxItemIndicator/MenuCheckboxItemIndicator';
+
export { MenuItem as Item } from './Item/MenuItem';
export { useMenuItem } from './Item/useMenuItem';