react-collapsible/Concepts/Conditional classes
Concepts~4 min

Conditional classes

The className prop on Collapsible is state-aware: prefix a class with open: or closed: and it applies only in that state. Because the flip is timed to the collapse animation itself, a single transition utility is all it takes to fade or slide content in sync with the height.

Three kinds of class

Collapsible doesn't pass className through verbatim — it parses it. Each whitespace-separated token is one of three kinds: open:-prefixed classes apply only while the panel is open, closed:-prefixed classes only while it's closed, and unprefixed classes always apply.

The resolved classes land on the inner content wrapper — the element that wraps your children and carries the [open] attribute — not the outer container that animates the grid. Height animation and content styling live on separate elements, so neither interferes with the other.

TSXparsing.tsx
<Collapsible
id='conditional-classes-panel'
open={open}
className='details-card open:opacity-100 closed:opacity-0 closed:-translate-y-2'
>
...
</Collapsible>

// What toggles on the inner content wrapper:
//
//   while open    opacity-100
//   while closed  opacity-0 -translate-y-2
//   always        details-card
//
// The prefixed tokens themselves also stay in the class list in both
// states — only their unprefixed forms toggle. That detail matters for
// Tailwind, covered below.

Flipping in sync with the animation

Conditional classes only earn their keep if they change at the right moment. When open flips to true, Collapsible defers the class switch by one requestAnimationFrame so the browser registers the pre-transition styles before it lands — the same deferral described in controlled state. Conditional classes resolve against that deferred state, so they transition in sync with the expand instead of snapping to their open values before it. Closing isn't deferred: closed: classes apply the moment the collapse starts.

The practical consequence: pair open:opacity-100 closed:opacity-0 with a transition-opacity utility and the content cross-fades in lockstep with the height animation. No effects, no timers, no orchestration — toggle open and both run together.

Shipping

Orders placed before 14:00 ship the same day. Tracking is emailed as soon as the carrier scans the parcel.

The panel height animates via the collapse; this content fades and slides through open: and closed: classes on the same tick.

TSXdetails-panel.tsx
import { useState } from 'react'
import { Collapsible } from '@westopp/react-collapsible'

const DetailsPanel = () => {
const [open, setOpen] = useState(false)
return (
  <>
    <button type='button' aria-controls='conditional-classes-fade-panel' aria-expanded={open} onClick={() => setOpen(!open)}>
      {open ? 'Hide details' : 'Show details'}
    </button>
    <Collapsible
      id='conditional-classes-fade-panel'
      open={open}
      className='transition-all duration-500 open:translate-y-0 open:opacity-100 closed:-translate-y-2 closed:opacity-0'
    >
      <p>Orders placed before 14:00 ship the same day.</p>
    </Collapsible>
  </>
)
}

Recipes

Three className strings cover most content animations. Match the utility's duration to the collapse — 500ms by default, or whatever you pass as transitionDuration (see custom transitions).

TSXrecipes.tsx
// Fade
className='transition-opacity duration-500 open:opacity-100 closed:opacity-0'

// Fade + slide
className='transition-all duration-500 open:translate-y-0 open:opacity-100 closed:-translate-y-2 closed:opacity-0'

// Staggered
className='transition-opacity duration-300 open:opacity-100 open:delay-200 closed:opacity-0 closed:delay-0'

The staggered recipe shows that the prefixes aren't limited to visual properties: open:delay-200 holds the fade back only on open, so the panel starts growing before the content arrives — while closing fades out immediately, so nothing lingers in a shrinking box.

  • Order #1042 — delivered
  • Order #1043 — in transit
  • Order #1044 — processing

Triggers are styled differently

Conditional classes style the panel's content. The trigger — chevron rotation, an active background — lives outside the Collapsible, so open: and closed: never reach it. Style it with plain CSS against the aria-expanded attribute that the accessibility contract already requires on every trigger.

CSStrigger.css
.chevron {
transition: transform 500ms;
}

[aria-expanded='true'] .chevron {
transform: rotate(180deg);
}