diff --git a/docs/data/base/components/collapsible/collapsible.md b/docs/data/base/components/collapsible/collapsible.md
new file mode 100644
index 0000000000..806953818c
--- /dev/null
+++ b/docs/data/base/components/collapsible/collapsible.md
@@ -0,0 +1,56 @@
+---
+productId: base-ui
+title: React Collapsible components
+components: CollapsibleRoot, CollapsibleTrigger, CollapsibleContent
+hooks: useCollapsibleRoot, useCollapsibleTrigger, useCollapsibleContent
+githubLabel: 'component: collapsible'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
+packageName: '@base_ui/react'
+---
+
+# Collapsible
+
+
Collapsible is a component that shows or hides content.
+
+{{"component": "@mui/docs/ComponentLinkHeader", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+## Installation
+
+Base UI components are all available as a single package.
+
+
+
+```bash npm
+npm install @base_ui/react
+```
+
+```bash yarn
+yarn add @base_ui/react
+```
+
+```bash pnpm
+pnpm add @base_ui/react
+```
+
+
+
+Once you have the package installed, import the component.
+
+```ts
+import * as Collapsible from '@base_ui/react/Collapsible';
+```
+
+## Anatomy
+
+- ` ` is a top-level component that facilitates communication between other components. It does not render to the DOM.
+- ` ` is the trigger element, a `` by default, that toggles the open/closed state of the content
+- ` ` is component that contains the Collapsible's content
+
+```tsx
+
+ Toggle
+ This is the content
+
+```
diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts
index 0ce753a28b..ce62afb1fe 100644
--- a/docs/data/base/pages.ts
+++ b/docs/data/base/pages.ts
@@ -39,6 +39,7 @@ const pages: readonly MuiPage[] = [
pathname: '/base-ui/components/data-display',
subheader: 'data-display',
children: [
+ { pathname: '/base-ui/react-collapsible', title: 'Collapsible' },
{ pathname: '/base-ui/react-popover', title: 'Popover' },
{ pathname: '/base-ui/react-tooltip', title: 'Tooltip' },
],
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index d9d16fe2e6..94cfe5c0f3 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -42,6 +42,18 @@ module.exports = [
'/base-ui/react-click-away-listener/components-api/#click-away-listener',
title: 'ClickAwayListener',
},
+ {
+ pathname: '/base-ui/react-collapsible/components-api/#collapsible-content',
+ title: 'CollapsibleContent',
+ },
+ {
+ pathname: '/base-ui/react-collapsible/components-api/#collapsible-root',
+ title: 'CollapsibleRoot',
+ },
+ {
+ pathname: '/base-ui/react-collapsible/components-api/#collapsible-trigger',
+ title: 'CollapsibleTrigger',
+ },
{
pathname: '/base-ui/react-transitions/components-api/#css-animation',
title: 'CssAnimation',
@@ -256,6 +268,18 @@ module.exports = [
pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox-root',
title: 'useCheckboxRoot',
},
+ {
+ pathname: '/base-ui/react-collapsible/hooks-api/#use-collapsible-content',
+ title: 'useCollapsibleContent',
+ },
+ {
+ pathname: '/base-ui/react-collapsible/hooks-api/#use-collapsible-root',
+ title: 'useCollapsibleRoot',
+ },
+ {
+ pathname: '/base-ui/react-collapsible/hooks-api/#use-collapsible-trigger',
+ title: 'useCollapsibleTrigger',
+ },
{
pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-close',
title: 'useDialogClose',
diff --git a/docs/pages/base-ui/api/collapsible-content.json b/docs/pages/base-ui/api/collapsible-content.json
new file mode 100644
index 0000000000..779c8c6153
--- /dev/null
+++ b/docs/pages/base-ui/api/collapsible-content.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "CollapsibleContent",
+ "imports": [
+ "import * as Collapsible from '@base_ui/react/Collapsible';\nconst CollapsibleContent = Collapsible.Content;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "CollapsibleContent",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/Collapsible/Content/CollapsibleContent.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/collapsible-root.json b/docs/pages/base-ui/api/collapsible-root.json
new file mode 100644
index 0000000000..7447809e42
--- /dev/null
+++ b/docs/pages/base-ui/api/collapsible-root.json
@@ -0,0 +1,20 @@
+{
+ "props": {
+ "defaultOpen": { "type": { "name": "bool" }, "default": "true" },
+ "disabled": { "type": { "name": "bool" }, "default": "false" },
+ "onOpenChange": { "type": { "name": "func" } },
+ "open": { "type": { "name": "bool" } }
+ },
+ "name": "CollapsibleRoot",
+ "imports": [
+ "import * as Collapsible from '@base_ui/react/Collapsible';\nconst CollapsibleRoot = Collapsible.Root;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": null,
+ "muiName": "CollapsibleRoot",
+ "filename": "/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/collapsible-trigger.json b/docs/pages/base-ui/api/collapsible-trigger.json
new file mode 100644
index 0000000000..589540e0ee
--- /dev/null
+++ b/docs/pages/base-ui/api/collapsible-trigger.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "union", "description": "element | func" } }
+ },
+ "name": "CollapsibleTrigger",
+ "imports": [
+ "import * as Collapsible from '@base_ui/react/Collapsible';\nconst CollapsibleTrigger = Collapsible.Trigger;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "CollapsibleTrigger",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/use-collapsible-content.json b/docs/pages/base-ui/api/use-collapsible-content.json
new file mode 100644
index 0000000000..0b55112ea7
--- /dev/null
+++ b/docs/pages/base-ui/api/use-collapsible-content.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useCollapsibleContent",
+ "filename": "/packages/mui-base/src/Collapsible/Content/useCollapsibleContent.ts",
+ "imports": ["import { useCollapsibleContent } from '@base_ui/react/Collapsible';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-collapsible-root.json b/docs/pages/base-ui/api/use-collapsible-root.json
new file mode 100644
index 0000000000..05006c027f
--- /dev/null
+++ b/docs/pages/base-ui/api/use-collapsible-root.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useCollapsibleRoot",
+ "filename": "/packages/mui-base/src/Collapsible/Root/useCollapsibleRoot.ts",
+ "imports": ["import { useCollapsibleRoot } from '@base_ui/react/Collapsible';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-collapsible-trigger.json b/docs/pages/base-ui/api/use-collapsible-trigger.json
new file mode 100644
index 0000000000..4ba8a78578
--- /dev/null
+++ b/docs/pages/base-ui/api/use-collapsible-trigger.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useCollapsibleTrigger",
+ "filename": "/packages/mui-base/src/Collapsible/Trigger/useCollapsibleTrigger.ts",
+ "imports": ["import { useCollapsibleTrigger } from '@base_ui/react/Collapsible';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/react-collapsible/[docsTab]/index.js b/docs/pages/base-ui/react-collapsible/[docsTab]/index.js
new file mode 100644
index 0000000000..8e5d534772
--- /dev/null
+++ b/docs/pages/base-ui/react-collapsible/[docsTab]/index.js
@@ -0,0 +1,96 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/collapsible/collapsible.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import CollapsibleContentApiJsonPageContent from '../../api/collapsible-content.json';
+import CollapsibleRootApiJsonPageContent from '../../api/collapsible-root.json';
+import CollapsibleTriggerApiJsonPageContent from '../../api/collapsible-trigger.json';
+import useCollapsibleContentApiJsonPageContent from '../../api/use-collapsible-content.json';
+import useCollapsibleRootApiJsonPageContent from '../../api/use-collapsible-root.json';
+import useCollapsibleTriggerApiJsonPageContent from '../../api/use-collapsible-trigger.json';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
+
+export const getStaticPaths = () => {
+ return {
+ paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }],
+ fallback: false, // can also be true or 'blocking'
+ };
+};
+
+export const getStaticProps = () => {
+ const CollapsibleContentApiReq = require.context(
+ 'docs-base/translations/api-docs/collapsible-content',
+ false,
+ /\.\/collapsible-content.*.json$/,
+ );
+ const CollapsibleContentApiDescriptions = mapApiPageTranslations(CollapsibleContentApiReq);
+
+ const CollapsibleRootApiReq = require.context(
+ 'docs-base/translations/api-docs/collapsible-root',
+ false,
+ /\.\/collapsible-root.*.json$/,
+ );
+ const CollapsibleRootApiDescriptions = mapApiPageTranslations(CollapsibleRootApiReq);
+
+ const CollapsibleTriggerApiReq = require.context(
+ 'docs-base/translations/api-docs/collapsible-trigger',
+ false,
+ /\.\/collapsible-trigger.*.json$/,
+ );
+ const CollapsibleTriggerApiDescriptions = mapApiPageTranslations(CollapsibleTriggerApiReq);
+
+ const useCollapsibleContentApiReq = require.context(
+ 'docs-base/translations/api-docs/use-collapsible-content',
+ false,
+ /\.\/use-collapsible-content.*.json$/,
+ );
+ const useCollapsibleContentApiDescriptions = mapApiPageTranslations(useCollapsibleContentApiReq);
+
+ const useCollapsibleRootApiReq = require.context(
+ 'docs-base/translations/api-docs/use-collapsible-root',
+ false,
+ /\.\/use-collapsible-root.*.json$/,
+ );
+ const useCollapsibleRootApiDescriptions = mapApiPageTranslations(useCollapsibleRootApiReq);
+
+ const useCollapsibleTriggerApiReq = require.context(
+ 'docs-base/translations/api-docs/use-collapsible-trigger',
+ false,
+ /\.\/use-collapsible-trigger.*.json$/,
+ );
+ const useCollapsibleTriggerApiDescriptions = mapApiPageTranslations(useCollapsibleTriggerApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ CollapsibleContent: CollapsibleContentApiDescriptions,
+ CollapsibleRoot: CollapsibleRootApiDescriptions,
+ CollapsibleTrigger: CollapsibleTriggerApiDescriptions,
+ },
+ componentsApiPageContents: {
+ CollapsibleContent: CollapsibleContentApiJsonPageContent,
+ CollapsibleRoot: CollapsibleRootApiJsonPageContent,
+ CollapsibleTrigger: CollapsibleTriggerApiJsonPageContent,
+ },
+ hooksApiDescriptions: {
+ useCollapsibleContent: useCollapsibleContentApiDescriptions,
+ useCollapsibleRoot: useCollapsibleRootApiDescriptions,
+ useCollapsibleTrigger: useCollapsibleTriggerApiDescriptions,
+ },
+ hooksApiPageContents: {
+ useCollapsibleContent: useCollapsibleContentApiJsonPageContent,
+ useCollapsibleRoot: useCollapsibleRootApiJsonPageContent,
+ useCollapsibleTrigger: useCollapsibleTriggerApiJsonPageContent,
+ },
+ },
+ };
+};
diff --git a/docs/pages/base-ui/react-collapsible/index.js b/docs/pages/base-ui/react-collapsible/index.js
new file mode 100644
index 0000000000..3755cb91b0
--- /dev/null
+++ b/docs/pages/base-ui/react-collapsible/index.js
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/collapsible/collapsible.md?@mui/markdown';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
diff --git a/docs/pages/experiments/collapsible.tsx b/docs/pages/experiments/collapsible.tsx
new file mode 100644
index 0000000000..173063cc72
--- /dev/null
+++ b/docs/pages/experiments/collapsible.tsx
@@ -0,0 +1,119 @@
+import * as React from 'react';
+import { useTheme } from '@mui/system';
+import * as Collapsible from '@base_ui/react/Collapsible';
+
+export default function CollapsibleDemo() {
+ return (
+
+
+
+
+
+
+
+
+ Trigger
+
+
+ This is the collapsed content
+
+
+
+
+ );
+}
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+export function Styles() {
+ const isDarkMode = useIsDarkMode();
+ return (
+
+ );
+}
diff --git a/docs/translations/api-docs/collapsible-content/collapsible-content.json b/docs/translations/api-docs/collapsible-content/collapsible-content.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/collapsible-content/collapsible-content.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/collapsible-root/collapsible-root.json b/docs/translations/api-docs/collapsible-root/collapsible-root.json
new file mode 100644
index 0000000000..fbfc79e653
--- /dev/null
+++ b/docs/translations/api-docs/collapsible-root/collapsible-root.json
@@ -0,0 +1,14 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "defaultOpen": {
+ "description": "If true
, the Collapsible is initially open. This is the uncontrolled counterpart of open
."
+ },
+ "disabled": { "description": "If true
, the component is disabled." },
+ "onOpenChange": { "description": "Callback fired when the Collapsible is opened or closed." },
+ "open": {
+ "description": "If true
, the Collapsible is initially open. This is the controlled counterpart of defaultOpen
."
+ }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/collapsible-trigger/collapsible-trigger.json b/docs/translations/api-docs/collapsible-trigger/collapsible-trigger.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/collapsible-trigger/collapsible-trigger.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-collapsible-content/use-collapsible-content.json b/docs/translations/api-docs/use-collapsible-content/use-collapsible-content.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-collapsible-content/use-collapsible-content.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/docs/translations/api-docs/use-collapsible-root/use-collapsible-root.json b/docs/translations/api-docs/use-collapsible-root/use-collapsible-root.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-collapsible-root/use-collapsible-root.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/docs/translations/api-docs/use-collapsible-trigger/use-collapsible-trigger.json b/docs/translations/api-docs/use-collapsible-trigger/use-collapsible-trigger.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-collapsible-trigger/use-collapsible-trigger.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/packages/mui-base/src/Collapsible/Content/CollapsibleContent.test.tsx b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.test.tsx
new file mode 100644
index 0000000000..f8730ecbf7
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.test.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Collapsible from '@base_ui/react/Collapsible';
+import { CollapsibleContext } from '@base_ui/react/Collapsible';
+import { describeConformance } from '../../../test/describeConformance';
+import type { CollapsibleContextValue } from '../Root/CollapsibleRoot.types';
+
+const contextValue: CollapsibleContextValue = {
+ contentId: 'ContentId',
+ disabled: false,
+ open: true,
+ setContentId() {},
+ setOpen() {},
+ ownerState: {
+ open: true,
+ disabled: false,
+ },
+};
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ inheritComponent: 'div',
+ render: (node) => {
+ const { container, ...other } = render(
+ {node} ,
+ );
+
+ return { container, ...other };
+ },
+ refInstanceof: window.HTMLDivElement,
+ }));
+});
diff --git a/packages/mui-base/src/Collapsible/Content/CollapsibleContent.tsx b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.tsx
new file mode 100644
index 0000000000..bb44181404
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.tsx
@@ -0,0 +1,56 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useCollapsibleContext } from '../Root/CollapsibleContext';
+import { collapsibleStyleHookMapping } from '../Root/styleHooks';
+import { useCollapsibleContent } from './useCollapsibleContent';
+import { CollapsibleContentProps } from './CollapsibleContent.types';
+
+const CollapsibleContent = React.forwardRef(function CollapsibleContent(
+ props: CollapsibleContentProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, render, ...otherProps } = props;
+
+ const { open, contentId, setContentId, ownerState } = useCollapsibleContext();
+
+ const { getRootProps } = useCollapsibleContent({
+ id: contentId,
+ open,
+ setContentId,
+ });
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'div',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: collapsibleStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+CollapsibleContent.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]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { CollapsibleContent };
diff --git a/packages/mui-base/src/Collapsible/Content/CollapsibleContent.types.ts b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.types.ts
new file mode 100644
index 0000000000..6c7d72beed
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Content/CollapsibleContent.types.ts
@@ -0,0 +1,20 @@
+import { BaseUIComponentProps } from '../../utils/types';
+import { CollapsibleRootOwnerState } from '../Root/CollapsibleRoot.types';
+
+export interface CollapsibleContentProps
+ extends BaseUIComponentProps<'div', CollapsibleRootOwnerState> {}
+
+export interface UseCollapsibleContentParameters {
+ id?: React.HTMLAttributes['id'];
+ /**
+ * The open state of the Collapsible
+ */
+ open: boolean;
+ setContentId: (id: string | undefined) => void;
+}
+
+export interface UseCollapsibleContentReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'button'>,
+ ) => React.ComponentPropsWithRef<'button'>;
+}
diff --git a/packages/mui-base/src/Collapsible/Content/useCollapsibleContent.ts b/packages/mui-base/src/Collapsible/Content/useCollapsibleContent.ts
new file mode 100644
index 0000000000..e521ca0002
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Content/useCollapsibleContent.ts
@@ -0,0 +1,48 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
+import { useId } from '../../utils/useId';
+import {
+ UseCollapsibleContentParameters,
+ UseCollapsibleContentReturnValue,
+} from './CollapsibleContent.types';
+/**
+ *
+ * Demos:
+ *
+ * - [Collapsible](https://mui.com/base-ui/react-collapsible/#hooks)
+ *
+ * API:
+ *
+ * - [useCollapsibleContent API](https://mui.com/base-ui/react-collapsible/hooks-api/#use-collapsible-content)
+ */
+function useCollapsibleContent(
+ parameters: UseCollapsibleContentParameters,
+): UseCollapsibleContentReturnValue {
+ const { id: idParam, open, setContentId } = parameters;
+
+ const id = useId(idParam);
+
+ useEnhancedEffect(() => {
+ setContentId(id);
+ return () => {
+ setContentId(undefined);
+ };
+ }, [id, setContentId]);
+
+ const getRootProps: UseCollapsibleContentReturnValue['getRootProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps(externalProps, {
+ id,
+ hidden: open ? undefined : 'hidden',
+ }),
+ [id, open],
+ );
+
+ return {
+ getRootProps,
+ };
+}
+
+export { useCollapsibleContent };
diff --git a/packages/mui-base/src/Collapsible/Root/CollapsibleContext.tsx b/packages/mui-base/src/Collapsible/Root/CollapsibleContext.tsx
new file mode 100644
index 0000000000..44cff8ac7a
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/CollapsibleContext.tsx
@@ -0,0 +1,22 @@
+'use client';
+import * as React from 'react';
+import { CollapsibleContextValue } from './CollapsibleRoot.types';
+
+/**
+ * @ignore - internal component.
+ */
+export const CollapsibleContext = React.createContext(
+ undefined,
+);
+
+if (process.env.NODE_ENV !== 'production') {
+ CollapsibleContext.displayName = 'CollapsibleContext';
+}
+
+export function useCollapsibleContext() {
+ const context = React.useContext(CollapsibleContext);
+ if (context === undefined) {
+ throw new Error('useCollapsibleContext must be used inside a Collapsible component');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.test.tsx b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.test.tsx
new file mode 100644
index 0000000000..88cf7143ce
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.test.tsx
@@ -0,0 +1,100 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Collapsible from '@base_ui/react/Collapsible';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describe('ARIA attributes', () => {
+ it('sets ARIA attributes', async () => {
+ const { getByTestId, getByRole } = await render(
+
+
+
+ ,
+ );
+
+ const trigger = getByRole('button');
+ const content = getByTestId('content');
+
+ expect(trigger).to.have.attribute('aria-expanded');
+
+ expect(trigger.getAttribute('aria-controls')).to.equal(content.getAttribute('id'));
+ });
+ });
+
+ describe('open state', () => {
+ it('controlled mode', async () => {
+ const { getByTestId, getByRole, setProps } = await render(
+
+
+
+ ,
+ );
+
+ const trigger = getByRole('button');
+ const content = getByTestId('content');
+
+ expect(trigger).to.have.attribute('aria-expanded', 'false');
+ expect(content).to.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'closed');
+
+ setProps({ open: true });
+
+ expect(trigger).to.have.attribute('aria-expanded', 'true');
+ expect(content).to.not.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'open');
+ });
+
+ it('uncontrolled mode', async () => {
+ const { getByTestId, getByRole, user } = await render(
+
+
+
+ ,
+ );
+
+ const trigger = getByRole('button');
+ const content = getByTestId('content');
+
+ expect(trigger).to.have.attribute('aria-expanded', 'true');
+ expect(content).to.not.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'open');
+
+ await user.pointer({ keys: '[MouseLeft]', target: trigger });
+
+ expect(trigger).to.have.attribute('aria-expanded', 'false');
+ expect(content).to.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'closed');
+ });
+ });
+
+ describe('keyboard interactions', () => {
+ ['Enter'].forEach((key) => {
+ it(`key: ${key} should toggle the Collapsible`, async () => {
+ const { getByTestId, getByRole, user } = await render(
+
+ Trigger
+
+ ,
+ );
+
+ const trigger = getByRole('button');
+ const content = getByTestId('content');
+
+ expect(trigger).to.have.attribute('aria-expanded', 'true');
+ expect(content).to.not.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'open');
+
+ await user.keyboard('[Tab]');
+ expect(trigger).toHaveFocus();
+ await user.keyboard(`[${key}]`);
+
+ expect(trigger).to.have.attribute('aria-expanded', 'false');
+ expect(content).to.have.attribute('hidden');
+ expect(content).to.have.attribute('data-state', 'closed');
+ });
+ });
+ });
+});
diff --git a/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx
new file mode 100644
index 0000000000..3a4320c792
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.tsx
@@ -0,0 +1,60 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useCollapsibleRoot } from './useCollapsibleRoot';
+import { CollapsibleContext } from './CollapsibleContext';
+import { CollapsibleContextValue, CollapsibleRootProps } from './CollapsibleRoot.types';
+
+function CollapsibleRoot(props: CollapsibleRootProps) {
+ const { open, defaultOpen, onOpenChange, disabled, children } = props;
+
+ const collapsible = useCollapsibleRoot({
+ open,
+ defaultOpen,
+ onOpenChange,
+ disabled,
+ });
+
+ const contextValue: CollapsibleContextValue = React.useMemo(
+ () => ({
+ ...collapsible,
+ ownerState: { open: collapsible.open, disabled: collapsible.disabled },
+ }),
+ [collapsible],
+ );
+
+ return {children} ;
+}
+
+CollapsibleRoot.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,
+ /**
+ * If `true`, the Collapsible is initially open.
+ * This is the uncontrolled counterpart of `open`.
+ * @default true
+ */
+ defaultOpen: PropTypes.bool,
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled: PropTypes.bool,
+ /**
+ * Callback fired when the Collapsible is opened or closed.
+ */
+ onOpenChange: PropTypes.func,
+ /**
+ * If `true`, the Collapsible is initially open.
+ * This is the controlled counterpart of `defaultOpen`.
+ */
+ open: PropTypes.bool,
+} as any;
+
+export { CollapsibleRoot };
diff --git a/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.types.ts b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.types.ts
new file mode 100644
index 0000000000..1e56238880
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/CollapsibleRoot.types.ts
@@ -0,0 +1,46 @@
+export interface CollapsibleContextValue extends UseCollapsibleRootReturnValue {
+ ownerState: Pick;
+}
+
+export type CollapsibleRootOwnerState = Pick;
+
+export interface CollapsibleRootProps extends UseCollapsibleRootParameters {
+ children: React.ReactNode;
+}
+
+export interface UseCollapsibleRootParameters {
+ /**
+ * If `true`, the Collapsible is initially open.
+ * This is the controlled counterpart of `defaultOpen`.
+ */
+ open?: boolean;
+ /**
+ * If `true`, the Collapsible is initially open.
+ * This is the uncontrolled counterpart of `open`.
+ * @default true
+ */
+ defaultOpen?: boolean;
+ /**
+ * Callback fired when the Collapsible is opened or closed.
+ */
+ onOpenChange?: (open: boolean) => void;
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+}
+
+export interface UseCollapsibleRootReturnValue {
+ contentId: React.HTMLAttributes['id'];
+ /**
+ * The disabled state of the Collapsible
+ */
+ disabled: boolean;
+ /**
+ * The open state of the Collapsible
+ */
+ open: boolean;
+ setContentId: (id: string | undefined) => void;
+ setOpen: (open: boolean) => void;
+}
diff --git a/packages/mui-base/src/Collapsible/Root/styleHooks.ts b/packages/mui-base/src/Collapsible/Root/styleHooks.ts
new file mode 100644
index 0000000000..29af054402
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/styleHooks.ts
@@ -0,0 +1,8 @@
+import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
+import type { CollapsibleRootOwnerState } from './CollapsibleRoot.types';
+
+export const collapsibleStyleHookMapping: CustomStyleHookMapping = {
+ open: (value) => {
+ return value ? { 'data-state': 'open' } : { 'data-state': 'closed' };
+ },
+};
diff --git a/packages/mui-base/src/Collapsible/Root/useCollapsibleRoot.ts b/packages/mui-base/src/Collapsible/Root/useCollapsibleRoot.ts
new file mode 100644
index 0000000000..bf6767fd3d
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Root/useCollapsibleRoot.ts
@@ -0,0 +1,47 @@
+'use client';
+import * as React from 'react';
+import { useControlled } from '../../utils/useControlled';
+import { useId } from '../../utils/useId';
+import {
+ UseCollapsibleRootParameters,
+ UseCollapsibleRootReturnValue,
+} from './CollapsibleRoot.types';
+/**
+ *
+ * Demos:
+ *
+ * - [Collapsible](https://mui.com/base-ui/react-collapsible/#hooks)
+ *
+ * API:
+ *
+ * - [useCollapsibleRoot API](https://mui.com/base-ui/react-collapsible/hooks-api/#use-collapsible-root)
+ */
+function useCollapsibleRoot(
+ parameters: UseCollapsibleRootParameters,
+): UseCollapsibleRootReturnValue {
+ const { open: openParam, defaultOpen = true, onOpenChange, disabled = false } = parameters;
+
+ const [open, setOpen] = useControlled({
+ controlled: openParam,
+ default: defaultOpen,
+ name: 'CollapsibleRoot',
+ });
+
+ const [contentId, setContentId] = React.useState(useId());
+
+ React.useEffect(() => {
+ if (onOpenChange) {
+ onOpenChange(open);
+ }
+ }, [onOpenChange, open]);
+
+ return {
+ contentId,
+ disabled,
+ open,
+ setContentId,
+ setOpen,
+ };
+}
+
+export { useCollapsibleRoot };
diff --git a/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.test.tsx b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.test.tsx
new file mode 100644
index 0000000000..f722bcae1a
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.test.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Collapsible from '@base_ui/react/Collapsible';
+import { CollapsibleContext } from '@base_ui/react/Collapsible';
+import { describeConformance } from '../../../test/describeConformance';
+import type { CollapsibleContextValue } from '../Root/CollapsibleRoot.types';
+
+const contextValue: CollapsibleContextValue = {
+ contentId: 'ContentId',
+ disabled: false,
+ open: true,
+ setContentId() {},
+ setOpen() {},
+ ownerState: {
+ open: true,
+ disabled: false,
+ },
+};
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ inheritComponent: 'button',
+ render: (node) => {
+ const { container, ...other } = render(
+ {node} ,
+ );
+
+ return { container, ...other };
+ },
+ refInstanceof: window.HTMLButtonElement,
+ }));
+});
diff --git a/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.tsx b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.tsx
new file mode 100644
index 0000000000..7ff4b86458
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.tsx
@@ -0,0 +1,56 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useCollapsibleContext } from '../Root/CollapsibleContext';
+import { collapsibleStyleHookMapping } from '../Root/styleHooks';
+import { useCollapsibleTrigger } from './useCollapsibleTrigger';
+import { CollapsibleTriggerProps } from './CollapsibleTrigger.types';
+
+const CollapsibleTrigger = React.forwardRef(function CollapsibleTrigger(
+ props: CollapsibleTriggerProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, render, ...otherProps } = props;
+
+ const { contentId, open, setOpen, ownerState } = useCollapsibleContext();
+
+ const { getRootProps } = useCollapsibleTrigger({
+ contentId,
+ open,
+ setOpen,
+ });
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'button',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: collapsibleStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+CollapsibleTrigger.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]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { CollapsibleTrigger };
diff --git a/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.types.ts b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.types.ts
new file mode 100644
index 0000000000..072a5478c9
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Trigger/CollapsibleTrigger.types.ts
@@ -0,0 +1,23 @@
+import { BaseUIComponentProps } from '../../utils/types';
+import { CollapsibleRootOwnerState } from '../Root/CollapsibleRoot.types';
+
+export interface CollapsibleTriggerProps
+ extends BaseUIComponentProps<'button', CollapsibleRootOwnerState> {}
+
+export interface UseCollapsibleTriggerParameters {
+ contentId: React.HTMLAttributes['id'];
+ /**
+ * The open state of the Collapsible
+ */
+ open: boolean;
+ /**
+ * A state setter that toggles the open state of the Collapsiblew
+ */
+ setOpen: (open: boolean) => void;
+}
+
+export interface UseCollapsibleTriggerReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'button'>,
+ ) => React.ComponentPropsWithRef<'button'>;
+}
diff --git a/packages/mui-base/src/Collapsible/Trigger/useCollapsibleTrigger.ts b/packages/mui-base/src/Collapsible/Trigger/useCollapsibleTrigger.ts
new file mode 100644
index 0000000000..7e5aa72e40
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/Trigger/useCollapsibleTrigger.ts
@@ -0,0 +1,41 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import {
+ UseCollapsibleTriggerParameters,
+ UseCollapsibleTriggerReturnValue,
+} from './CollapsibleTrigger.types';
+/**
+ *
+ * Demos:
+ *
+ * - [Collapsible](https://mui.com/base-ui/react-collapsible/#hooks)
+ *
+ * API:
+ *
+ * - [useCollapsibleTrigger API](https://mui.com/base-ui/react-collapsible/hooks-api/#use-collapsible-trigger)
+ */
+function useCollapsibleTrigger(
+ parameters: UseCollapsibleTriggerParameters,
+): UseCollapsibleTriggerReturnValue {
+ const { contentId, open, setOpen } = parameters;
+
+ const getRootProps: UseCollapsibleTriggerReturnValue['getRootProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'button'>(externalProps, {
+ type: 'button',
+ 'aria-controls': contentId,
+ 'aria-expanded': open,
+ onClick() {
+ setOpen(!open);
+ },
+ }),
+ [contentId, open, setOpen],
+ );
+
+ return {
+ getRootProps,
+ };
+}
+
+export { useCollapsibleTrigger };
diff --git a/packages/mui-base/src/Collapsible/index.barrel.ts b/packages/mui-base/src/Collapsible/index.barrel.ts
new file mode 100644
index 0000000000..de6c88d343
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/index.barrel.ts
@@ -0,0 +1,20 @@
+export { CollapsibleRoot } from './Root/CollapsibleRoot';
+export type * from './Root/CollapsibleRoot.types';
+export { useCollapsibleRoot } from './Root/useCollapsibleRoot';
+export * from './Root/CollapsibleContext';
+
+export { CollapsibleTrigger } from './Trigger/CollapsibleTrigger';
+export type {
+ CollapsibleTriggerProps as TriggerProps,
+ UseCollapsibleTriggerParameters,
+ UseCollapsibleTriggerReturnValue,
+} from './Trigger/CollapsibleTrigger.types';
+export { useCollapsibleTrigger } from './Trigger/useCollapsibleTrigger';
+
+export { CollapsibleContent } from './Content/CollapsibleContent';
+export type {
+ CollapsibleContentProps as ContentProps,
+ UseCollapsibleContentParameters,
+ UseCollapsibleContentReturnValue,
+} from './Content/CollapsibleContent.types';
+export { useCollapsibleContent } from './Content/useCollapsibleContent';
diff --git a/packages/mui-base/src/Collapsible/index.ts b/packages/mui-base/src/Collapsible/index.ts
new file mode 100644
index 0000000000..5cb2830bf4
--- /dev/null
+++ b/packages/mui-base/src/Collapsible/index.ts
@@ -0,0 +1,26 @@
+export { CollapsibleRoot as Root } from './Root/CollapsibleRoot';
+export {
+ CollapsibleRootOwnerState as CollapsibleOwnerState,
+ CollapsibleRootProps as RootProps,
+ UseCollapsibleRootParameters,
+ UseCollapsibleRootReturnValue,
+ CollapsibleContextValue,
+} from './Root/CollapsibleRoot.types';
+export { useCollapsibleRoot } from './Root/useCollapsibleRoot';
+export * from './Root/CollapsibleContext';
+
+export { CollapsibleTrigger as Trigger } from './Trigger/CollapsibleTrigger';
+export type {
+ CollapsibleTriggerProps as TriggerProps,
+ UseCollapsibleTriggerParameters,
+ UseCollapsibleTriggerReturnValue,
+} from './Trigger/CollapsibleTrigger.types';
+export { useCollapsibleTrigger } from './Trigger/useCollapsibleTrigger';
+
+export { CollapsibleContent as Content } from './Content/CollapsibleContent';
+export type {
+ CollapsibleContentProps as ContentProps,
+ UseCollapsibleContentParameters,
+ UseCollapsibleContentReturnValue,
+} from './Content/CollapsibleContent.types';
+export { useCollapsibleContent } from './Content/useCollapsibleContent';