Collapsible
The complete component reference. react-collapsible ships one controlled component — this page is every prop it takes, what each one does, and where the edge cases live.
Imports
The package root exports the component (named) and its props type. The stylesheet is a separate subpath export — import it once at your app root, or nothing animates.
import { Collapsible } from '@westopp/react-collapsible' import type { CollapsibleProps } from '@westopp/react-collapsible' // once, at the app root import '@westopp/react-collapsible/styles.css'
Props
Three props are required — children, id, open. Everything else has a default or passes through.
| Prop | Type | Description |
|---|---|---|
| children | ReactNode | Required. The content to collapse. Rendered inside the inner wrapper; the component never unmounts it. |
| id | string | Required. Set as the id of the outer container. Your trigger points at it with aria-controls — see below. |
| open | boolean | Required. Whether the panel is expanded. Fully controlled; the component holds no open state of its own. |
| direction | 'x' | 'y' | Collapse axis: 'y' animates height, 'x' animates width. Default: 'y'. |
| transitionDuration | string | Any CSS time value, e.g. '300ms'. Overrides the 500ms stylesheet default and style.transitionDuration. |
| initialOpen | boolean | Skips the entry animation on first paint for a panel that mounts open. Default: false. |
| hideMode | 'remove' | 'hide' | What collapsed content becomes: 'remove' takes it out of the flow with display none; 'hide' keeps it in the DOM, invisible. Default: 'remove'. |
| className | string | Applied to the inner wrapper. Supports open: and closed: conditional prefixes. |
| style | CSSProperties | Merged onto the outer container. The transitionDuration prop wins over a transitionDuration key here. |
| ...divProps | HTMLAttributes<HTMLDivElement> | Every other div prop — data-*, role, event handlers — spreads onto the outer container. id is excluded from the passthrough type. |
id
id lands on the outer container and is the component's half of the accessibility contract: the trigger must carry aria-controls equal to this id, plus aria-expanded mirroring the state. The id is how assistive technology connects the button to the panel it controls.
In development builds, the first time an instance opens, the component checks the document for an element with aria-controls matching its id — and warns once in the console if none exists. Production builds skip the check entirely.
const [open, setOpen] = useState(false) <button aria-controls='collapsible-filters' aria-expanded={open} onClick={() => setOpen(!open)}> Filters </button> <Collapsible id='collapsible-filters' open={open}> <FilterPanel /> </Collapsible>
open
The component is fully controlled — open is the single source of truth, and toggling lives in your code: useState, a reducer, a store selector, anything that yields a boolean.
One internal subtlety worth knowing: when open flips to true, the grid transition starts immediately, but the [open] attribute and any open:/closed: conditional classes flip one animation frame later (via requestAnimationFrame) so the browser registers their pre-transition styles first — that deferral is what makes transitions keyed on them fire reliably. Closing applies immediately. Controlled state covers this end to end.
// any boolean source works const open = useUiStore((s) => s.detailsOpen) <Collapsible id='collapsible-details' open={open}> <Details /> </Collapsible>
direction
'y' (the default) animates grid-template-rows — a height collapse. 'x' animates grid-template-columns — a width collapse. Horizontal collapse has two layout requirements: the parent typically needs display: flex so the container can shrink to zero width, and the content usually wants white-space: nowrap or a fixed width so text doesn't reflow mid-animation. The horizontal collapse guide works through both.
<div className='flex'> <Collapsible id='collapsible-sidebar' open={open} direction='x'> <nav className='w-64 whitespace-nowrap'>...</nav> </Collapsible> <main>...</main> </div>
transitionDuration
Any CSS time value — '300ms', '0.5s'. The stylesheet defaults to 500ms; this prop is inlined as a style on the outer container, so it overrides both the stylesheet default and any transitionDuration key in the style prop. For easing curves and motion preferences, see custom transitions.
<Collapsible id='collapsible-notice' open={open} transitionDuration='250ms'> <Notice /> </Collapsible>
initialOpen
A panel that mounts with open={true} would otherwise animate from collapsed on first paint. initialOpen suppresses that: the component forces transitionDuration: '0s' for the first frame, then restores the configured duration. The panel appears open instantly, and every toggle after that animates normally.
initialOpen only skips the animation — it does not open the panel. Pair it with state that starts true.
const [open, setOpen] = useState(true) <Collapsible id='collapsible-summary' open={open} initialOpen> <Summary /> </Collapsible>
hideMode
'remove' (the default) gives collapsed content display: none — it leaves the document flow and the accessibility tree. 'hide' keeps display: grid but applies visibility: hidden, so the content stays in the DOM: form input state, iframes, and measurements all survive a collapse. In hide mode the inner wrapper also carries aria-hidden mirroring the state, so screen readers skip the invisible content. The trade-offs are covered in hide modes.
<Collapsible id='collapsible-payment' open={open} hideMode='hide'> {/* input state survives collapse */} <PaymentForm /> </Collapsible>
className
Applied to the inner content wrapper, not the outer container. Two prefixes make classes conditional: open:foo applies foo only while expanded, closed:bar applies bar only while collapsed; unprefixed classes always apply. The conditional classes flip on a RAF-deferred copy of open, so paired transition-* utilities fire reliably and run in sync with the collapse animation — fade content as the panel grows, for instance. Full treatment in conditional classes.
<Collapsible id='collapsible-menu' open={open} className='transition-opacity duration-500 open:opacity-100 closed:opacity-0' > <Menu /> </Collapsible>
style
Merged onto the outer container — the element that owns the grid transition. A transitionDuration key here works as an override of the stylesheet default, but loses to the transitionDuration prop when both are set, and to the forced 0s during an initialOpen first frame.
<Collapsible id='collapsible-panel' open={open} style={{ transitionDuration: '300ms', marginTop: 8 }}> <Panel /> </Collapsible>
Everything else
All remaining div props — data-* attributes, role, event handlers — spread onto the outer container. id is the one exception: it is omitted from the passthrough type and always set from the id prop, because the accessibility contract depends on it.
<Collapsible id='collapsible-results' open={open} data-testid='results-panel' onTransitionEnd={() => console.log('collapse finished')} > <Results /> </Collapsible>