diff --git a/docs/pages/experiments/accordion.tsx b/docs/pages/experiments/accordion.tsx
new file mode 100644
index 0000000000..66d75b3f13
--- /dev/null
+++ b/docs/pages/experiments/accordion.tsx
@@ -0,0 +1,73 @@
+import * as React from 'react';
+import * as Accordion from '@base_ui/react/Accordion';
+
+export default function App() {
+ return (
+
+
Plain HTML
+
+
+
+
+
+
+ This the contents of Panel 1
+
+
+
+
+
+
+
+
+ This the contents of Panel 2
+
+
+
+
+
+
+
+
+
+
+
Base UI
+
+
+
+
+ Trigger 1
+
+
+ This is the contents of Accordion.Panel 1
+
+
+
+
+
+ Trigger 2
+
+
+ This is the contents of Accordion.Panel 2
+
+
+
+
+ );
+}
diff --git a/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx b/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx
new file mode 100644
index 0000000000..17c40954a1
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Heading/AccordionHeading.tsx
@@ -0,0 +1,33 @@
+'use client';
+import * as React from 'react';
+import { BaseUIComponentProps } from '../../utils/types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import type { AccordionSection } from '../Section/AccordionSection';
+import { useAccordionSectionContext } from '../Section/AccordionSectionContext';
+import { accordionStyleHookMapping } from '../Section/styleHooks';
+
+const AccordionHeading = React.forwardRef(function AccordionHeading(
+ props: AccordionHeading.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...otherProps } = props;
+
+ const { ownerState } = useAccordionSectionContext();
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'h3',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: accordionStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+export { AccordionHeading };
+
+export namespace AccordionHeading {
+ export interface Props extends BaseUIComponentProps<'h3', AccordionSection.OwnerState> {}
+}
diff --git a/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx b/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx
new file mode 100644
index 0000000000..4795e869eb
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Panel/AccordionPanel.tsx
@@ -0,0 +1,55 @@
+'use client';
+import * as React from 'react';
+import { BaseUIComponentProps } from '../../utils/types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useCollapsibleContext } from '../../Collapsible/Root/CollapsibleContext';
+import { useCollapsibleContent } from '../../Collapsible/Content/useCollapsibleContent';
+import type { AccordionSection } from '../Section/AccordionSection';
+import { useAccordionSectionContext } from '../Section/AccordionSectionContext';
+import { accordionStyleHookMapping } from '../Section/styleHooks';
+
+export const AccordionPanel = React.forwardRef(function AccordionPanel(
+ props: AccordionPanel.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, htmlHidden, render, ...otherProps } = props;
+
+ const { animated, mounted, open, contentId, setContentId, setMounted, setOpen } =
+ useCollapsibleContext();
+
+ const { getRootProps, height } = useCollapsibleContent({
+ animated,
+ htmlHidden,
+ id: contentId,
+ mounted,
+ open,
+ ref: forwardedRef,
+ setContentId,
+ setMounted,
+ setOpen,
+ });
+
+ const { ownerState } = useAccordionSectionContext();
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'div',
+ ownerState,
+ className,
+ extraProps: {
+ ...otherProps,
+ style: {
+ '--accordion-content-height': height ? `${height}px` : undefined,
+ },
+ },
+ customStyleHookMapping: accordionStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+export namespace AccordionPanel {
+ export interface Props
+ extends BaseUIComponentProps<'div', AccordionSection.OwnerState>,
+ Pick {}
+}
diff --git a/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx b/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx
new file mode 100644
index 0000000000..b90f9f7deb
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Root/AccordionRoot.tsx
@@ -0,0 +1,73 @@
+'use client';
+import * as React from 'react';
+import { FloatingList } from '@floating-ui/react';
+import { BaseUIComponentProps } from '../../utils/types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useAccordionRoot } from './useAccordionRoot';
+import { AccordionRootContext } from './AccordionRootContext';
+
+const AccordionRoot = React.forwardRef(function AccordionRoot(
+ props: AccordionRoot.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { animated, disabled, defaultValue, value, className, render, ...otherProps } = props;
+
+ const { getRootProps, ...accordion } = useAccordionRoot({
+ animated,
+ disabled,
+ defaultValue,
+ value,
+ });
+
+ const ownerState: AccordionRoot.OwnerState = React.useMemo(
+ () => ({
+ value: accordion.value,
+ disabled: accordion.disabled,
+ // transitionStatus: accordion.transitionStatus,
+ }),
+ [accordion.value, accordion.disabled],
+ );
+
+ const contextValue: AccordionRoot.Context = React.useMemo(
+ () => ({
+ ...accordion,
+ ownerState,
+ }),
+ [accordion, ownerState],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'div',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: {
+ value: () => null,
+ },
+ });
+
+ return (
+
+ {renderElement()}
+
+ );
+});
+
+export { AccordionRoot };
+
+export namespace AccordionRoot {
+ export interface Context extends Omit {
+ ownerState: OwnerState;
+ }
+
+ export interface OwnerState {
+ value: useAccordionRoot.Value;
+ disabled: boolean;
+ }
+
+ export interface Props
+ extends useAccordionRoot.Parameters,
+ BaseUIComponentProps {}
+}
diff --git a/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx b/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx
new file mode 100644
index 0000000000..0f862f8dd6
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Root/AccordionRootContext.tsx
@@ -0,0 +1,22 @@
+'use client';
+import * as React from 'react';
+import type { AccordionRoot } from './AccordionRoot';
+
+/**
+ * @ignore - internal component.
+ */
+export const AccordionRootContext = React.createContext(
+ undefined,
+);
+
+if (process.env.NODE_ENV !== 'production') {
+ AccordionRootContext.displayName = 'AccordionRootContext';
+}
+
+export function useAccordionRootContext() {
+ const context = React.useContext(AccordionRootContext);
+ if (context === undefined) {
+ throw new Error('useAccordionRootContext must be used inside a Accordion component');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Accordion/Root/styleHooks.ts b/packages/mui-base/src/Accordion/Root/styleHooks.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
new file mode 100644
index 0000000000..7d5c028682
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
@@ -0,0 +1,107 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import { useControlled } from '../../utils/useControlled';
+
+export function useAccordionRoot(
+ parameters: useAccordionRoot.Parameters,
+): useAccordionRoot.ReturnValue {
+ const { animated = true, defaultValue, value: valueParam, disabled = false } = parameters;
+
+ const accordionSectionRefs = React.useRef<(HTMLElement | null)[]>([]);
+
+ const [value, setValue] = useControlled({
+ controlled: valueParam,
+ default: valueParam ?? defaultValue ?? [],
+ name: 'Accordion',
+ state: 'value',
+ });
+ // console.log(value);
+
+ const handleOpenChange = React.useCallback(
+ (newValue: number | string, nextOpen: boolean) => {
+ // console.group('useAccordionRoot handleOpenChange');
+ // console.log('newValue', newValue, 'nextOpen', nextOpen, 'openValues', value);
+ if (nextOpen) {
+ const nextOpenValues = value.slice();
+ nextOpenValues.push(newValue);
+ setValue(nextOpenValues);
+ } else {
+ const nextOpenValues = value.filter((v) => v !== newValue);
+ setValue(nextOpenValues);
+ }
+ // console.groupEnd();
+ },
+ [value],
+ );
+
+ const getRootProps = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps(externalProps, {
+ role: 'region',
+ }),
+ [],
+ );
+
+ return React.useMemo(
+ () => ({
+ getRootProps,
+ accordionSectionRefs,
+ animated,
+ disabled,
+ handleOpenChange,
+ value,
+ }),
+ [getRootProps, accordionSectionRefs, animated, disabled, handleOpenChange, value],
+ );
+}
+
+export namespace useAccordionRoot {
+ export type Value = readonly (string | number)[];
+
+ export interface Parameters {
+ /**
+ * If `true`, the component supports CSS/JS-based animations and transitions.
+ * @default true
+ */
+ animated?: boolean;
+ /**
+ * The value of the currently open `Accordion.Section`
+ * This is the controlled counterpart of `defaultValue`.
+ */
+ value?: Value;
+ /**
+ * The default value representing the currently open `Accordion.Section`
+ * This is the uncontrolled counterpart of `value`.
+ * @default 0
+ */
+ defaultValue?: Value;
+ /**
+ * Callback fired when an Accordion section is opened or closed.
+ * The value representing the involved section is provided as an argument.
+ */
+ onOpenChange?: (value: Value) => void;
+ /**
+ * If `true`, the component is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ }
+
+ export interface ReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'div'>,
+ ) => React.ComponentPropsWithRef<'div'>;
+ accordionSectionRefs: React.MutableRefObject<(HTMLElement | null)[]>;
+ animated: boolean;
+ /**
+ * The disabled state of the Accordion
+ */
+ disabled: boolean;
+ handleOpenChange: (value: number | string, nextOpen: boolean) => void;
+ /**
+ * The open state of the Accordion
+ */
+ value: Value;
+ }
+}
diff --git a/packages/mui-base/src/Accordion/Section/AccordionSection.tsx b/packages/mui-base/src/Accordion/Section/AccordionSection.tsx
new file mode 100644
index 0000000000..7f0909c3ae
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Section/AccordionSection.tsx
@@ -0,0 +1,128 @@
+'use client';
+import * as React from 'react';
+import { useListItem } from '@floating-ui/react';
+import { useForkRef } from '../../utils/useForkRef';
+import { BaseUIComponentProps } from '../../utils/types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import type { TransitionStatus } from '../../utils/useTransitionStatus';
+import { useCollapsibleRoot } from '../../Collapsible/Root/useCollapsibleRoot';
+import type { CollapsibleRoot } from '../../Collapsible/Root/CollapsibleRoot';
+import { CollapsibleContext } from '../../Collapsible/Root/CollapsibleContext';
+import type { AccordionRoot } from '../Root/AccordionRoot';
+import { useAccordionRootContext } from '../Root/AccordionRootContext';
+import { AccordionSectionContext } from './AccordionSectionContext';
+// import { useAccordionSection } from './useAccordionSection';
+import { accordionStyleHookMapping } from './styleHooks';
+
+const AccordionSection = React.forwardRef(function AccordionSection(
+ props: AccordionSection.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, render, value: valueProp, ...otherProps } = props;
+
+ const sectionRef = React.useRef(null);
+ const { ref: listItemRef, index } = useListItem();
+ const mergedRef = useForkRef(forwardedRef, listItemRef, sectionRef);
+
+ const {
+ animated,
+ disabled,
+ handleOpenChange,
+ ownerState: rootOwnerState,
+ value: openValues,
+ } = useAccordionRootContext();
+
+ const value = valueProp ?? index;
+
+ const isOpen = React.useMemo(() => {
+ if (!openValues) {
+ return false;
+ }
+
+ for (let i = 0; i < openValues.length; i += 1) {
+ if (openValues[i] === value) {
+ return true;
+ }
+ }
+
+ return false;
+ }, [openValues, value]);
+
+ const collapsible = useCollapsibleRoot({
+ animated,
+ open: isOpen,
+ onOpenChange: (nextOpen) => handleOpenChange(value, nextOpen),
+ disabled,
+ });
+
+ const collapsibleOwnerState: CollapsibleRoot.OwnerState = React.useMemo(
+ () => ({
+ open: collapsible.open,
+ disabled: collapsible.disabled,
+ transitionStatus: collapsible.transitionStatus,
+ }),
+ [collapsible.open, collapsible.disabled, collapsible.transitionStatus],
+ );
+
+ const collapsibleContext: CollapsibleRoot.Context = React.useMemo(
+ () => ({
+ ...collapsible,
+ ownerState: collapsibleOwnerState,
+ }),
+ [collapsible, collapsibleOwnerState],
+ );
+
+ const ownerState: AccordionSection.OwnerState = React.useMemo(
+ () => ({
+ ...rootOwnerState,
+ index,
+ open: isOpen,
+ transitionStatus: collapsible.transitionStatus,
+ }),
+ [collapsible.transitionStatus, index, isOpen, rootOwnerState],
+ );
+
+ const accordionSectionContext: AccordionSection.Context = React.useMemo(
+ () => ({
+ open: isOpen,
+ ownerState,
+ }),
+ [isOpen, ownerState],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'div',
+ className,
+ ownerState,
+ ref: mergedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: accordionStyleHookMapping,
+ });
+
+ return (
+
+
+ {renderElement()}
+
+
+ );
+});
+
+export { AccordionSection };
+
+export namespace AccordionSection {
+ export interface Context {
+ open: boolean;
+ ownerState: OwnerState;
+ }
+
+ export interface OwnerState extends AccordionRoot.OwnerState {
+ index: number;
+ open: boolean;
+ transitionStatus: TransitionStatus;
+ }
+
+ export interface Props extends BaseUIComponentProps {
+ value: number | string;
+ }
+}
diff --git a/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx b/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx
new file mode 100644
index 0000000000..291a16a06a
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Section/AccordionSectionContext.tsx
@@ -0,0 +1,24 @@
+'use client';
+import * as React from 'react';
+import type { AccordionSection } from './AccordionSection';
+
+/**
+ * @ignore - internal component.
+ */
+export const AccordionSectionContext = React.createContext(
+ undefined,
+);
+
+if (process.env.NODE_ENV !== 'production') {
+ AccordionSectionContext.displayName = 'AccordionSectionContext';
+}
+
+export function useAccordionSectionContext() {
+ const context = React.useContext(AccordionSectionContext);
+ if (context === undefined) {
+ throw new Error(
+ 'useAccordionSectionContext must be used inside the component',
+ );
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Accordion/Section/styleHooks.ts b/packages/mui-base/src/Accordion/Section/styleHooks.ts
new file mode 100644
index 0000000000..64e04c4cf1
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Section/styleHooks.ts
@@ -0,0 +1,21 @@
+import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
+import type { AccordionSection } from './AccordionSection';
+
+export const accordionStyleHookMapping: CustomStyleHookMapping = {
+ index: (value) => {
+ return Number.isInteger(value) ? { 'data-index': String(value) } : null;
+ },
+ open: (value) => {
+ return value ? { 'data-state': 'open' } : { 'data-state': 'closed' };
+ },
+ transitionStatus: (value) => {
+ if (value === 'entering') {
+ return { 'data-entering': '' } as Record;
+ }
+ if (value === 'exiting') {
+ return { 'data-exiting': '' };
+ }
+ return null;
+ },
+ value: () => null,
+};
diff --git a/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx
new file mode 100644
index 0000000000..c60dcaceeb
--- /dev/null
+++ b/packages/mui-base/src/Accordion/Trigger/AccordionTrigger.tsx
@@ -0,0 +1,44 @@
+'use client';
+import * as React from 'react';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { BaseUIComponentProps } from '../../utils/types';
+import { useCollapsibleContext } from '../../Collapsible/Root/CollapsibleContext';
+import { useCollapsibleTrigger } from '../../Collapsible/Trigger/useCollapsibleTrigger';
+import type { AccordionSection } from '../Section/AccordionSection';
+import { useAccordionSectionContext } from '../Section/AccordionSectionContext';
+import { accordionStyleHookMapping } from '../Section/styleHooks';
+
+const AccordionTrigger = React.forwardRef(function AccordionTrigger(
+ props: AccordionTrigger.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, render, ...otherProps } = props;
+
+ const { contentId, open, setOpen } = useCollapsibleContext();
+
+ const { getRootProps } = useCollapsibleTrigger({
+ contentId,
+ open,
+ setOpen,
+ });
+
+ const { ownerState } = useAccordionSectionContext();
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'button',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: accordionStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+export { AccordionTrigger };
+
+namespace AccordionTrigger {
+ export interface Props extends BaseUIComponentProps<'button', AccordionSection.OwnerState> {}
+}
diff --git a/packages/mui-base/src/Accordion/index.barrel.ts b/packages/mui-base/src/Accordion/index.barrel.ts
new file mode 100644
index 0000000000..0d057e0615
--- /dev/null
+++ b/packages/mui-base/src/Accordion/index.barrel.ts
@@ -0,0 +1,5 @@
+export * from './Root/AccordionRoot';
+export * from './Section/AccordionSection';
+export * from './Heading/AccordionHeading';
+export * from './Trigger/AccordionTrigger';
+export * from './Panel/AccordionPanel';
diff --git a/packages/mui-base/src/Accordion/index.ts b/packages/mui-base/src/Accordion/index.ts
new file mode 100644
index 0000000000..7a9efb6c15
--- /dev/null
+++ b/packages/mui-base/src/Accordion/index.ts
@@ -0,0 +1,13 @@
+export { AccordionRoot as Root } from './Root/AccordionRoot';
+export { useAccordionRoot } from './Root/useAccordionRoot';
+export { AccordionRootContext, useAccordionRootContext } from './Root/AccordionRootContext';
+
+export { AccordionSection as Section } from './Section/AccordionSection';
+export {
+ AccordionSectionContext,
+ useAccordionSectionContext,
+} from './Section/AccordionSectionContext';
+
+export { AccordionHeading as Heading } from './Heading/AccordionHeading';
+export { AccordionTrigger as Trigger } from './Trigger/AccordionTrigger';
+export { AccordionPanel as Panel } from './Panel/AccordionPanel';
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 7f29893f01..24c3e01687 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -1,3 +1,4 @@
+export * from './Accordion/index.barrel';
export * from './AlertDialog/index.barrel';
export * from './Checkbox/index.barrel';
export * from './Dialog/index.barrel';
diff --git a/packages/mui-base/src/utils/defaultRenderFunctions.tsx b/packages/mui-base/src/utils/defaultRenderFunctions.tsx
index 4fd8e3ef4a..5bcd3fbc93 100644
--- a/packages/mui-base/src/utils/defaultRenderFunctions.tsx
+++ b/packages/mui-base/src/utils/defaultRenderFunctions.tsx
@@ -11,6 +11,10 @@ export const defaultRenderFunctions = {
// eslint-disable-next-line jsx-a11y/heading-has-content
return ;
},
+ h3: (props: React.ComponentPropsWithRef<'h3'>) => {
+ // eslint-disable-next-line jsx-a11y/heading-has-content
+ return ;
+ },
output: (props: React.ComponentPropsWithRef<'output'>) => {
return ;
},