Collapsible

Collapsible is a component that shows or hides content.

Give FeedbackWAI-ARIABundle Size

This is the collapsed content. The element that shows and hides the content has role button

When the content is visible, the element with role `button` has `aria-expanded` set to `true`

When the content area is hidden, it is set to `false`

Optionally, the element with role `button` has a value specified for `aria-controls` that refers to the element that contains all the content that is shown or hidden

'use client';
import * as React from 'react';
import { styled, useTheme, Box } from '@mui/system';
import { Collapsible as BaseCollapsible } from '@base_ui/react/Collapsible';

const Collapsible = BaseCollapsible.Root;

const CollapsibleTrigger = styled(BaseCollapsible.Trigger)`
  display: flex;
  flex-flow: row nowrap;
  justify-content: center;
  gap: 4px;
  font-size: 16px;

  & svg {
    margin-top: 1px;
  }

  &[data-state='open'] svg {
    transform: rotate(180deg);
  }
`;

const CollapsibleContent = styled(BaseCollapsible.Content)``;

export default function UnstyledCollapsibleIntroduction() {
  // Replace this with your app logic for determining dark mode
  const isDarkMode = useIsDarkMode();
  const [open, setOpen] = React.useState(true);
  return (
    <Box
      className={isDarkMode ? 'dark' : ''}
      sx={{ width: 480, fontFamily: 'IBM Plex Sans, sans-serif' }}
    >
      <Collapsible open={open} onOpenChange={setOpen}>
        <CollapsibleTrigger>
          <svg
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            width="16"
            height="16"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="m19.5 8.25-7.5 7.5-7.5-7.5"
            />
          </svg>
          Show {open ? 'less' : 'more'}
        </CollapsibleTrigger>
        <CollapsibleContent>
          <p>
            This is the collapsed content. The element that shows and hides the
            content has role button
          </p>
          <p>
            When the content is visible, the element with role `button` has
            `aria-expanded` set to `true`
          </p>
          <p>When the content area is hidden, it is set to `false`</p>
          <p>
            Optionally, the element with role `button` has a value specified for
            `aria-controls` that refers to the element that contains all the content
            that is shown or hidden
          </p>
        </CollapsibleContent>
      </Collapsible>
    </Box>
  );
}

function useIsDarkMode() {
  const theme = useTheme();
  return theme.palette.mode === 'dark';
}

Installation

Base UI components are all available as a single package.

npm install @base_ui/react

Once you have the package installed, import the component.

import { Collapsible } from '@base_ui/react/Collapsible';

Anatomy

<Collapsible.Root>
  <Collapsible.Trigger>Toggle</Collapsible.Trigger>
  <Collapsible.Content>This is the content</Collapsible.Content>
</Collapsible.Root>

Improving searchability of hidden content

This is not yet supported in Safari and Firefox as of August 2024 and will fall back to the default hidden behavior.

Content hidden in the Collapsible.Content component can be made accessible only to a browser's find-in-page functionality with the htmlHidden prop to improve searchability:

<Collapsible.Root defaultOpen={false}>
  <Collapsible.Trigger>Toggle</Collapsible.Trigger>
  <Collapsible.Content htmlHidden="until-found">
    When this component is closed, this sentence will only be accessible to the browser's native
    find-in-page functionality
  </Collapsible.Content>
</Collapsible.Root>

We recommend using CSS animations for animated collapsibles that use this feature. Currently there is browser bug that does not highlight the found text inside elements that have a CSS transition applied.

This relies on the HTML hidden="until-found" attribute which only has partial browser support as of August 2024, but automatically falls back to the default hidden state in unsupported browsers.

Animations

Animation states

Four states are available as data attributes to animate the Collapsible:

The component can be animate when opening or closing using either:

The height of the Content subcomponent is provided as the --collapsible-content-height CSS variable

CSS Animations

CSS animations can be used with two declarations:

.Collapsible-content {
  overflow: hidden;
}
 
.Collapsible-content[data-state='open'] {
  animation: slideDown 300ms ease-out;
}
 
.Collapsible-content[data-state='closed'] {
  animation: slideUp 300ms ease-in;
}
 
@keyframes slideDown {
  from {
    height: 0;
  }
  to {
    height: var(--collapsible-content-height);
  }
}
 
@keyframes slideUp {
  from {
    height: var(--collapsible-content-height);
  }
  to {
    height: 0;
  }
}

This is the collapsed content

This is the second paragraph

This is a longer sentence and also the third paragraph

'use client';
import * as React from 'react';
import { useTheme } from '@mui/system';
import { Collapsible } from '@base_ui/react/Collapsible';

export default function CssAnimatedCollapsible() {
  const [open, setOpen] = React.useState(true);
  return (
    <div className="CssAnimatedCollapsible">
      <Collapsible.Root open={open} onOpenChange={setOpen}>
        <Collapsible.Trigger className="CssAnimatedCollapsible-trigger">
          <span className="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 80 80"
              focusable="false"
            >
              <path d="M70.3 13.8L40 66.3 9.7 13.8z" />
            </svg>
          </span>
          Show {open ? 'less' : 'more'}
        </Collapsible.Trigger>
        <Collapsible.Content className="CssAnimatedCollapsible-content">
          <p>This is the collapsed content</p>
          <p>This is the second paragraph</p>
          <p>This is a longer sentence and also the third paragraph</p>
        </Collapsible.Content>
      </Collapsible.Root>
      <Styles />
    </div>
  );
}

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 (
    <style suppressHydrationWarning>{`
    .CssAnimatedCollapsible {
      font-family: system-ui, sans-serif;
      line-height: 1.4;
      width: 480px;
    }

    .CssAnimatedCollapsible h3 {
      color: ${isDarkMode ? 'cyan' : 'blue'};
    }

    .CssAnimatedCollapsible-trigger {
      border: 0;
      padding: .5rem 1rem .5rem 0;
      font: inherit;
      background-color: transparent;
    }

    .CssAnimatedCollapsible-trigger:hover {
      cursor: pointer;
    }

    .CssAnimatedCollapsible-trigger .icon {
      display: inline-block;
      font-size: 60%;
      color: #000;
      background-color: ${isDarkMode ? grey[50] : grey[700]};
      padding: 0.3em 0.2em 0 0.2em;
      border: 0;
      border-radius: 50%;
      line-height: 1;
      text-align: center;
      text-indent: 0;
      transform: rotate(270deg);
      margin-right: 0.6em;
    }

    .CssAnimatedCollapsible-trigger svg {
      width: 1.25em;
      height: 1.25em;
      fill: ${isDarkMode ? grey[900] : grey[300]};
      transition: transform 0.2s ease-in;
      transform-origin: center 45%;
    }

    .CssAnimatedCollapsible-trigger[data-state="open"] svg {
      transform: rotate(90deg);
    }

    .CssAnimatedCollapsible-content {
      background-color: ${isDarkMode ? grey[700] : grey[300]};
      overflow: hidden;
    }

    .CssAnimatedCollapsible-content p {
      padding: 0 1rem;
    }

    .CssAnimatedCollapsible-content[data-state='open'] {
      animation: slideDown 300ms ease-out;
    }
    .CssAnimatedCollapsible-content[data-state='closed'] {
      animation: slideUp 300ms ease-out;
    }

    @keyframes slideDown {
      from {
        height: 0;
      }
      to {
        height: var(--collapsible-content-height);
      }
    }

    @keyframes slideUp {
      from {
        height: var(--collapsible-content-height);
      }
      to {
        height: 0;
      }
    }
    `}</style>
  );
}

CSS Transitions

When using CSS transitions, styles for the Content subcomponent must be applied to three states:

.Collapsible-content {
  overflow: hidden;
}
 
.Collapsible2-content[data-state='open'] {
  height: var(--collapsible-content-height);
  transition: height 300ms ease-out;
}
 
.Collapsible-content[data-entering] {
  height: 0;
}
 
.Collapsible2-content[data-state='closed'] {
  height: 0;
  transition: height 300ms ease-in;
}

This is the collapsed content

This is the second paragraph

This is a longer sentence and also the third paragraph

'use client';
import * as React from 'react';
import { useTheme } from '@mui/system';
import { Collapsible } from '@base_ui/react/Collapsible';

export default function CssTransitionCollapsible() {
  const [open, setOpen] = React.useState(true);
  return (
    <div className="CssTransitionCollapsible">
      <Collapsible.Root open={open} onOpenChange={setOpen}>
        <Collapsible.Trigger className="CssTransitionCollapsible-trigger">
          <span className="icon">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 80 80"
              focusable="false"
            >
              <path d="M70.3 13.8L40 66.3 9.7 13.8z" />
            </svg>
          </span>
          Show {open ? 'less' : 'more'}
        </Collapsible.Trigger>
        <Collapsible.Content className="CssTransitionCollapsible-content">
          <p>This is the collapsed content</p>
          <p>This is the second paragraph</p>
          <p>This is a longer sentence and also the third paragraph</p>
        </Collapsible.Content>
      </Collapsible.Root>
      <Styles />
    </div>
  );
}

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 (
    <style suppressHydrationWarning>{`
    .CssTransitionCollapsible {
      font-family: system-ui, sans-serif;
      line-height: 1.4;
      width: 480px;
    }

    .CssTransitionCollapsible h3 {
      color: ${isDarkMode ? 'cyan' : 'blue'};
    }

    .CssTransitionCollapsible-trigger {
      border: 0;
      padding: .5rem 1rem .5rem 0;
      font: inherit;
      background-color: transparent;
    }

    .CssTransitionCollapsible-trigger:hover {
      cursor: pointer;
    }

    .CssTransitionCollapsible-trigger .icon {
      display: inline-block;
      font-size: 60%;
      color: #000;
      background-color: ${isDarkMode ? grey[50] : grey[700]};
      padding: 0.3em 0.2em 0 0.2em;
      border: 0;
      border-radius: 50%;
      line-height: 1;
      text-align: center;
      text-indent: 0;
      transform: rotate(270deg);
      margin-right: 0.6em;
    }

    .CssTransitionCollapsible-trigger svg {
      width: 1.25em;
      height: 1.25em;
      fill: ${isDarkMode ? grey[900] : grey[300]};
      transition: transform 0.2s ease-in;
      transform-origin: center 45%;
    }

    .CssTransitionCollapsible-trigger[data-state="open"] svg {
      transform: rotate(90deg);
    }

    .CssTransitionCollapsible-content {
      background-color: ${isDarkMode ? grey[700] : grey[300]};
      overflow: hidden;
    }

    .CssTransitionCollapsible-content p {
      padding: 0 1rem;
    }

    .CssTransitionCollapsible-content[data-state='open'] {
      height: var(--collapsible-content-height);
      transition: height 200ms ease-out;
    }

    .CssTransitionCollapsible-content[data-state='closed'] {
      height: 0;
      transition: height 200ms ease-in;
    }

    .CssTransitionCollapsible-content[data-entering] {
      height: 0;
    }
    `}</style>
  );
}

JavaScript Animations

When using external libraries for animation, for example framer-motion, be aware that Collapsible hides content using the html hidden attribute in the closed state, and does not unmount the Collapsible.Content subcomponent.

function App() {
  const [open, setOpen] = useState(false);
  return (
    <Collapsible.Root open={open} onOpenChange={setOpen}>
      <Collapsible.Trigger>Toggle</Collapsible.Trigger>
      <Collapsible.Content
        render={
          <motion.div
            key="CollapsibleContent"
            initial={false}
            animate={open ? 'open' : 'closed'}
            exit={!open ? 'open' : 'closed'}
            variants={{
              open: {
                height: 'auto',
                transition: { duration: 0.6, ease: 'ease-out' },
              },
              closed: {
                height: 0,
                transition: { duration: 0.6, ease: 'ease-in' },
                transitionEnd: { display: 'revert-layer' },
              },
            }}
          />
        }
      >
        This is the content
      </Collapsible.Content>
    </Collapsible.Root>
  );
}

Overriding default components

Use the render prop to override the rendered elements with your own components. The Collapsible.Root component does not render an element to the DOM by default, but can do so with the render prop:

// Element shorthand
<Collapsible.Root render={<MyCollapsibleRoot />} />
// Function
<Collapsible.Root render={(props) => <MyCollapsibleRoot {...props} />} />

API Reference

CollapsibleRoot

PropTypeDefaultDescription
animatedbooltrueIf true, the component supports CSS/JS-based animations and transitions.
classNameunionClass names applied to the element or a function that returns them based on the component's state.
defaultOpenbooltrueIf true, the Collapsible is initially open. This is the uncontrolled counterpart of open.
disabledboolfalseIf true, the component is disabled.
onOpenChangefuncCallback fired when the Collapsible is opened or closed.
openboolIf true, the Collapsible is initially open. This is the controlled counterpart of defaultOpen.
renderunionA function to customize rendering of the component.

CollapsibleTrigger

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
renderunionA function to customize rendering of the component.

CollapsibleContent

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
htmlHiddenenum'hidden'The hidden state when closed
renderunionA function to customize rendering of the component.

Contents