CSS classes
The stylesheet is part of the API. Every class the component generates is stable and overridable — this page is the full styling surface: which classes land on which wrapper, the shipped CSS line by line, and where to hook in your own.
Collapsible renders two divs. The outer container carries your id and the animation classes — it owns the grid track that does the collapsing. The inner wrapper (expander-content) holds your children and carries the state hooks you style against. Knowing which div carries what tells you where any selector should point.
Outer container classes
The outer div always carries exactly one base class, picked by direction and hideMode, and gains the matching -expanded class while open is true. Eight classes total — four base/expanded pairs:
| Class | Configuration | Applied |
|---|---|---|
| collapsible-y | direction: 'y' + hideMode: 'remove' (the defaults) | Always present on the outer container. |
| collapsible-y-expanded | direction: 'y' + hideMode: 'remove' | Added while open is true, removed when it turns false. |
| collapsible-x | direction: 'x' + hideMode: 'remove' | Always present on the outer container. |
| collapsible-x-expanded | direction: 'x' + hideMode: 'remove' | Added while open is true. |
| collapsible-y-hidden | direction: 'y' + hideMode: 'hide' | Always present on the outer container. |
| collapsible-y-hidden-expanded | direction: 'y' + hideMode: 'hide' | Added while open is true. |
| collapsible-x-hidden | direction: 'x' + hideMode: 'hide' | Always present on the outer container. |
| collapsible-x-hidden-expanded | direction: 'x' + hideMode: 'hide' | Added while open is true. |
The base class is the machinery: collapsed track, transition declarations, overflow: hidden. The -expanded class flips the track from 0fr to 1fr. The -hidden variants exist because hide mode swaps display: none for visibility: hidden — collapsed content stays in the DOM, so the whole transition pair changes.
Inner wrapper hooks
The inner wrapper carries three styling hooks of its own, plus an accessibility attribute in hide mode:
| Hook | Applied | Purpose |
|---|---|---|
| expander-content | always | Sets min-height: 0 and min-width: 0 so the grid track can crush the content. |
| expander-content-open | while open is true | Tracks the open prop directly, on the same render. |
| [open] attribute | one frame after open turns true | Bare attribute set on the RAF-deferred tick; removed immediately on close. |
| aria-hidden | hideMode: 'hide' only | true while collapsed, false while expanded — keeps hidden-but-mounted content out of the accessibility tree. |
expander-content is load-bearing, not decorative. Grid items default to min-content sizing, which means a 0fr track cannot shrink below the content's natural size — the collapse would stop at the text. min-height: 0 (and min-width: 0 for horizontal) removes that floor so the track can actually reach zero.
The timing difference between the last two class hooks matters. expander-content-open follows the open prop on the same render. The [open] attribute — like the open:/closed: conditional classes — flips one animation frame later, so the browser paints the closed styles first and any transition you attach animates instead of snapping. Closing applies immediately in both cases. The deferral is covered in controlled state.
[open] is the handiest hook for styling content by state without touching the component:
#css-classes-filters [open] { /* styles for the inner wrapper while expanded */ } #css-classes-filters [open] .summary-row { /* or anything inside it */ }
Tailwind users get this for free — the built-in open: variant targets the same attribute.
The shipped stylesheet
This is the entire CSS the package ships. Import it once at your app root from @westopp/react-collapsible/styles.css; everything below is fair game to override.
.collapsible-y { display: none; grid-template-rows: 0fr; transition: grid-template-rows 500ms, display 500ms; transition-behavior: allow-discrete; overflow: hidden; } .expander-content { min-height: 0; min-width: 0; } .collapsible-y.collapsible-y-expanded { display: grid; grid-template-rows: 1fr; @starting-style { grid-template-rows: 0fr; } } .collapsible-x { display: none; grid-template-columns: 0fr; transition: grid-template-columns 500ms, display 500ms; transition-behavior: allow-discrete; overflow: hidden; } .collapsible-x.collapsible-x-expanded { display: grid; grid-template-columns: 1fr; @starting-style { grid-template-columns: 0fr; } } .collapsible-y-hidden { display: grid; visibility: hidden; grid-template-rows: 0fr; transition: grid-template-rows 500ms, visibility 500ms; transition-behavior: allow-discrete; overflow: hidden; } .collapsible-y-hidden.collapsible-y-hidden-expanded { visibility: visible; grid-template-rows: 1fr; @starting-style { grid-template-rows: 0fr; } } .collapsible-x-hidden { display: grid; visibility: hidden; grid-template-columns: 0fr; transition: grid-template-columns 500ms, visibility 500ms; transition-behavior: allow-discrete; overflow: hidden; } .collapsible-x-hidden.collapsible-x-hidden-expanded { visibility: visible; grid-template-columns: 1fr; @starting-style { grid-template-columns: 0fr; } }
Three declarations carry the whole effect:
The transition pair. Each base class transitions two properties over the same 500ms: the fr track (grid-template-rows or grid-template-columns) interpolating between 0fr and 1fr — the visible collapse — and a discrete property (display in remove mode, visibility in hide mode) riding alongside it.
transition-behavior: allow-discrete. Discrete properties normally switch instantly, which would yank content out the moment a close begins. allow-discrete lets them participate in the transition: on close, display: none (or visibility: hidden) lands at the end of the 500ms, after the track has finished collapsing.
The @starting-style blocks. An element entering from display: none has no before-state to transition from, so opening would normally snap. @starting-style supplies that before-state — 0fr — giving the browser a starting value to animate away from. This is what makes the entry animation possible at all.
Styling hooks
Global overrides
To retheme every collapsible, target the class family in your own CSS, loaded after the package stylesheet so equal-specificity rules win:
.collapsible-y, .collapsible-x, .collapsible-y-hidden, .collapsible-x-hidden { transition-duration: 300ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); }
One caveat on duration: the transitionDuration prop is applied as an inline style, so it beats any class rule. Use the class override for your app-wide default and the prop for one-off exceptions.
Per-instance overrides
Every collapsible already has a unique id (the accessibility contract requires it), so a per-instance override is one selector away. Or pass style — it spreads onto the outer container:
import { Collapsible } from '@westopp/react-collapsible' import { useState } from 'react' function Filters() { const [open, setOpen] = useState(false) return ( <> <button aria-controls="css-classes-filters" aria-expanded={open} onClick={() => setOpen(!open)}> Filters </button> <Collapsible id="css-classes-filters" open={open} style={{ transitionTimingFunction: 'ease-in-out' }}> <p>Filter controls go here.</p> </Collapsible> </> ) }
/* equivalent, in CSS */ #css-classes-filters { transition-timing-function: ease-in-out; }
For duration specifically, reach for the transitionDuration prop instead — it exists for exactly this. Easing, motion preferences, and choreography patterns get a full treatment in custom transitions.
Styling your content
The wrappers impose nothing on your children — no padding, no typography, only the grid track on the outside and the min-size resets on expander-content. Style content exactly as you would anywhere else; classes on child elements pass through untouched. One thing to keep in mind: overflow: hidden on the outer container clips anything that extends past the panel, so shadows and popovers that must escape belong outside the collapsible.
For content that should change with state — fading text, rotating chevrons, color shifts — skip the manual [open] selectors and use the className prop's open:/closed: prefixes. They land on the inner wrapper, flip in sync with the collapse animation, and read declaratively at the call site. That route is covered in conditional classes.