diff --git a/docs/data/base/components/accordion/UnstyledAccordionIntroduction.js b/docs/data/base/components/accordion/UnstyledAccordionIntroduction.js
index dcfddb6413..33efa25cbf 100644
--- a/docs/data/base/components/accordion/UnstyledAccordionIntroduction.js
+++ b/docs/data/base/components/accordion/UnstyledAccordionIntroduction.js
@@ -1,3 +1,4 @@
+'use client';
import * as React from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import * as Accordion from '@base_ui/react/Accordion';
@@ -122,7 +123,7 @@ function Styles() {
margin: 12px auto 12px 0;
}
- .Accordion-trigger[data-state="open"] svg {
+ .Accordion-trigger[data-collapsible="open"] svg {
transform: rotate(180deg);
}
diff --git a/docs/data/base/components/accordion/UnstyledAccordionIntroduction.tsx b/docs/data/base/components/accordion/UnstyledAccordionIntroduction.tsx
index dcfddb6413..33efa25cbf 100644
--- a/docs/data/base/components/accordion/UnstyledAccordionIntroduction.tsx
+++ b/docs/data/base/components/accordion/UnstyledAccordionIntroduction.tsx
@@ -1,3 +1,4 @@
+'use client';
import * as React from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import * as Accordion from '@base_ui/react/Accordion';
@@ -122,7 +123,7 @@ function Styles() {
margin: 12px auto 12px 0;
}
- .Accordion-trigger[data-state="open"] svg {
+ .Accordion-trigger[data-collapsible="open"] svg {
transform: rotate(180deg);
}
diff --git a/docs/data/base/components/accordion/accordion.md b/docs/data/base/components/accordion/accordion.md
index 08d86d4335..70ebe45b7e 100644
--- a/docs/data/base/components/accordion/accordion.md
+++ b/docs/data/base/components/accordion/accordion.md
@@ -60,23 +60,15 @@ Accordions are implemented using a collection of related components:
-
- Toggle one
-
+ Toggle one
-
- Section one content
-
+ Section one content
-
- Toggle two
-
+ Toggle two
-
- Section two content
-
+ Section two content
```
@@ -94,23 +86,15 @@ You can optionally specify a custom `value` prop on `Section`:
-
- Toggle one
-
+ Toggle one
-
- Section one content
-
+ Section one content
-
- Toggle two
-
+ Toggle two
-
- Section two content
-
+ Section two content
```
@@ -121,51 +105,35 @@ When uncontrolled, use the `defaultValue` prop to set the initial state of the a
```tsx
- {/* `value={0}` by default */}
+
-
- Toggle one
-
+ Toggle one
-
- Section one content
-
+ Section one content
- {/* `value={1}` by default */}
+
-
- Toggle two
-
+ Toggle two
-
- Section two content
-
+ Section two content
-
+;
-{/* with custom `value`s */}
+// with custom `value`s
-
- Toggle one
-
+ Toggle one
-
- Section one content
-
+ Section one content
-
- Toggle two
-
+ Toggle two
-
- Section two content
-
+ Section two content
-
+;
```
### Controlled
@@ -179,26 +147,18 @@ return (
-
- Toggle one
-
+ Toggle one
-
- Section one content
-
+ Section one content
-
- Toggle two
-
+ Toggle two
-
- Section two content
-
+ Section two content
-)
+);
```
## Customization
@@ -208,9 +168,7 @@ return (
By default, all accordion sections can be opened at the same time. Use the `openMultiple` prop to only allow one open section at a time:
```tsx
-
- {/* subcomponents */}
-
+{/* subcomponents */}
```
### At least one section remains open
@@ -222,15 +180,15 @@ const [value, setValue] = React.useState([0]);
const handleOpenChange = (newValue) => {
if (newValue.length > 0) {
- setValue(newValue)
+ setValue(newValue);
}
-}
+};
return (
{/* subcomponents */}
-)
+);
```
## Horizontal
@@ -238,9 +196,7 @@ return (
Use the `orientation` prop to configure a horizontal accordion. In a horizontal accordion, focus will move between `Accordion.Trigger`s with the Right Arrow and Left Arrow keys, instead of Down/Up.
```tsx
-
- {/* subcomponents */}
-
+{/* subcomponents */}
```
## RTL
@@ -248,9 +204,152 @@ Use the `orientation` prop to configure a horizontal accordion. In a horizontal
Use the `direction` prop to configure a RTL accordion:
```tsx
-
- {/* subcomponents */}
-
+{/* subcomponents */}
```
When a horizontal accordion is set to `direction="rtl"`, keyboard actions are reversed accordingly - Left Arrow moves focus to the next trigger and Right Arrow moves focus to the previous trigger.
+
+## Improving searchability of hidden content
+
+:::warning
+This is [not yet supported](https://caniuse.com/mdn-html_global_attributes_hidden_until-found_value) in Safari and Firefox as of August 2024 and will fall back to the default `hidden` behavior.
+
+:::
+
+Content hidden by `Accordion.Panel` components can be made accessible only to a browser's find-in-page functionality with the `htmlHidden` prop to improve searchability:
+
+```js
+{/* subcomponents */}
+```
+
+Alternatively `htmlHidden` can be passed to `Accordion.Panel` directly to enable this for only one section instead of the whole accordion.
+
+We recommend using [CSS animations](#css-animations) for animated accordions that use this feature. Currently there is browser bug that does not highlight the found text inside elements that have a [CSS transition](#css-transitions) applied.
+
+This relies on the HTML `hidden="until-found"` attribute which only has [partial browser support](https://caniuse.com/mdn-html_global_attributes_hidden_until-found_value) as of August 2024, but automatically falls back to the default `hidden` state in unsupported browsers.
+
+## Animations
+
+Accordion uses [`Collapsible`](/base-ui/react-collapsible/) internally, and can be animated in a [similar way](/base-ui/react-collapsible/#animations).
+
+Four states are available as data attributes to animate the `Accordion.Panel`:
+
+- `[data-collapsible="open"]` - `open` state is `true`.
+- `[data-collapsible="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing.
+- `[data-entering]` - the `hidden` attribute was just removed from the DOM and the content element participates in page layout. The `data-entering` attribute will be removed 1 animation frame later.
+- `[data-exiting]` - the content element is in the process of being hidden from the DOM, but is still mounted.
+
+The component can be animate when opening or closing using either:
+
+- CSS animations
+- CSS transitions
+- JavaScript animations
+
+The dimensions of the `Accordion.Panel` subcomponent are provided as the `--accordion-content-height` and `--accordion-content-width` CSS variables.
+
+### CSS Animations
+
+CSS animations can be used with two declarations:
+
+```css
+.Accordion-panel {
+ overflow: hidden;
+}
+
+.Accordion-panel[data-collapsible='open'] {
+ animation: slideDown 300ms ease-out;
+}
+
+.Accordion-panel[data-collapsible='closed'] {
+ animation: slideUp 300ms ease-in;
+}
+
+@keyframes slideDown {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--accordion-content-height);
+ }
+}
+
+@keyframes slideUp {
+ from {
+ height: var(--accordion-content-height);
+ }
+ to {
+ height: 0;
+ }
+}
+```
+
+### CSS Transitions
+
+When using CSS transitions, styles for the `Panel` must be applied to three states:
+
+- The closed styles with `[data-collapsible="closed"]`
+- The open styles with `[data-collapsible="open"]`
+- The entering styles with `[data-entering]`
+
+```css
+.Accordion-panel {
+ overflow: hidden;
+}
+
+.Accordion-panel[data-collapsible='open'] {
+ height: var(--accordion-content-height);
+ transition: height 300ms ease-out;
+}
+
+.Accordion-panel[data-entering] {
+ height: 0;
+}
+
+.Accordion-panel[data-collapsible='closed'] {
+ height: 0;
+ transition: height 300ms ease-in;
+}
+```
+
+### JavaScript Animations
+
+When using external libraries for animation, for example `framer-motion`, be aware that `Accordion.Section`s hides content using the html `hidden` attribute in the closed state, and does not unmount from the DOM.
+
+```js
+function App() {
+ const [value, setValue] = useState([0]);
+ return (
+
+
+
+ Toggle
+
+
+ }
+ >
+ This is the content
+
+
+ {/* more accordion sections */}
+
+ );
+}
+```
diff --git a/docs/data/base/components/collapsible/collapsible.md b/docs/data/base/components/collapsible/collapsible.md
index 50f843d7d5..c57483f582 100644
--- a/docs/data/base/components/collapsible/collapsible.md
+++ b/docs/data/base/components/collapsible/collapsible.md
@@ -97,7 +97,7 @@ The component can be animate when opening or closing using either:
- CSS transitions
- JavaScript animations
-The height of the `Content` subcomponent is provided as the `--collapsible-content-height` CSS variable
+The dimensions of the `Content` subcomponent are provided as the `--collapsible-content-height` and `--collapsible-content-width` CSS variables
### CSS Animations
@@ -150,7 +150,7 @@ When using CSS transitions, styles for the `Content` subcomponent must be applie
overflow: hidden;
}
-.Collapsible2-content[data-state='open'] {
+.Collapsible-content[data-state='open'] {
height: var(--collapsible-content-height);
transition: height 300ms ease-out;
}
@@ -159,7 +159,7 @@ When using CSS transitions, styles for the `Content` subcomponent must be applie
height: 0;
}
-.Collapsible2-content[data-state='closed'] {
+.Collapsible-content[data-state='closed'] {
height: 0;
transition: height 300ms ease-in;
}
diff --git a/docs/pages/experiments/accordion-animations.tsx b/docs/pages/experiments/accordion-animations.tsx
new file mode 100644
index 0000000000..a5d8fa4d8a
--- /dev/null
+++ b/docs/pages/experiments/accordion-animations.tsx
@@ -0,0 +1,189 @@
+import * as React from 'react';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import * as Accordion from '@base_ui/react/Accordion';
+
+const DURATION = '300ms';
+
+export default function App() {
+ return (
+
+
CSS @keyframe animations + `hidden="until-found"`
+
+ {[0, 1, 2].map((index) => (
+
+
+
+ Trigger {index + 1}
+
+
+
+
+
+ This is the contents of Accordion.Panel {index + 1}
+
+ It uses `hidden="until-found"` and can be opened by the browser's
+ in-page search
+
+
+
+ ))}
+
+
+
CSS transitions
+
+ {[0, 1, 2].map((index) => (
+
+
+
+ Trigger {index + 1}
+
+
+
+
+ This is the contents of Accordion.Panel {index + 1}
+
+
+ ))}
+
+
+
+ );
+}
+
+function MaterialStyles() {
+ return (
+
+ );
+}
diff --git a/docs/pages/experiments/accordion-material.tsx b/docs/pages/experiments/accordion-material.tsx
deleted file mode 100644
index 3130c291ac..0000000000
--- a/docs/pages/experiments/accordion-material.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import * as React from 'react';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import * as Accordion from '@base_ui/react/Accordion';
-
-export default function App() {
- return (
-
-
- {[0, 1, 2, 3].map((index) => (
-
-
-
- Trigger {index + 1}
-
-
-
-
- This is the contents of Accordion.Panel {index + 1}
-
-
- ))}
-
-
-
- );
-}
-
-function MaterialStyles() {
- return (
-
- );
-}
diff --git a/packages/mui-base/src/Accordion/Root/styleHooks.ts b/packages/mui-base/src/Accordion/Root/styleHooks.ts
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
index 7a591fb2df..041726228a 100644
--- a/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
+++ b/packages/mui-base/src/Accordion/Root/useAccordionRoot.ts
@@ -7,6 +7,26 @@ import { ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT } from '../../Composite/c
const SUPPORTED_KEYS = [ARROW_DOWN, ARROW_UP, ARROW_RIGHT, ARROW_LEFT, 'Home', 'End'];
+function getActiveTriggers(accordionSectionRefs: {
+ current: (HTMLElement | null)[];
+}): HTMLButtonElement[] {
+ const { current: accordionSectionElements } = accordionSectionRefs;
+
+ const output: HTMLButtonElement[] = [];
+
+ for (let i = 0; i < accordionSectionElements.length; i += 1) {
+ const section = accordionSectionElements[i];
+ if (!isDisabled(section)) {
+ const trigger = section?.querySelector('[type="button"]') as HTMLButtonElement;
+ if (!isDisabled(trigger)) {
+ output.push(trigger);
+ }
+ }
+ }
+
+ return output;
+}
+
function isDisabled(element: HTMLElement | null) {
return (
element === null ||
@@ -49,8 +69,6 @@ export function useAccordionRoot(
const handleOpenChange = React.useCallback(
(newValue: number | string, nextOpen: boolean) => {
- // console.group('useAccordionRoot handleOpenChange');
- // console.log('newValue', newValue, 'nextOpen', nextOpen, 'openValues', value);
if (!openMultiple) {
const nextValue = value[0] === newValue ? [] : [newValue];
setValue(nextValue);
@@ -58,16 +76,13 @@ export function useAccordionRoot(
} else if (nextOpen) {
const nextOpenValues = value.slice();
nextOpenValues.push(newValue);
- // console.log('nextOpenValues', nextOpenValues);
setValue(nextOpenValues);
onOpenChange(nextOpenValues);
} else {
const nextOpenValues = value.filter((v) => v !== newValue);
- // console.log('nextOpenValues', nextOpenValues);
setValue(nextOpenValues);
onOpenChange(nextOpenValues);
}
- // console.groupEnd();
},
[onOpenChange, openMultiple, setValue, value],
);
@@ -84,21 +99,9 @@ export function useAccordionRoot(
return;
}
- // console.group('onKeyDown');
- const { current: accordionSectionElements } = accordionSectionRefs;
-
- // TODO: memo this outside
- const triggers: HTMLButtonElement[] = [];
+ event.preventDefault();
- for (let i = 0; i < accordionSectionElements.length; i += 1) {
- const section = accordionSectionElements[i];
- if (!isDisabled(section)) {
- const trigger = section?.querySelector('[type="button"]') as HTMLButtonElement;
- if (!isDisabled(trigger)) {
- triggers.push(trigger);
- }
- }
- }
+ const triggers = getActiveTriggers(accordionSectionRefs);
const numOfEnabledTriggers = triggers.length;
const lastIndex = numOfEnabledTriggers - 1;
@@ -155,10 +158,8 @@ export function useAccordionRoot(
}
if (nextIndex > -1) {
- // console.log('focus nextIndex', nextIndex);
triggers[nextIndex].focus();
}
- // console.groupEnd();
},
});
},
diff --git a/packages/mui-base/src/Accordion/Section/styleHooks.ts b/packages/mui-base/src/Accordion/Section/styleHooks.ts
index cb56d531b6..09c3917327 100644
--- a/packages/mui-base/src/Accordion/Section/styleHooks.ts
+++ b/packages/mui-base/src/Accordion/Section/styleHooks.ts
@@ -12,7 +12,7 @@ export const accordionStyleHookMapping: CustomStyleHookMapping {
- return value ? { 'data-state': 'open' } : { 'data-state': 'closed' };
+ return value ? { 'data-collapsible': 'open' } : { 'data-collapsible': 'closed' };
},
transitionStatus: (value) => {
if (value === 'entering') {