react-collapsible/Getting started
Getting started~5 min

Getting started

A working, animated, accessible disclosure panel in four steps. Install, wire a button to the open prop, polish the motion with conditional classes and a custom duration, then decide how the panel should mount. You write no animation code — the browser owns the motion.

Here's the destination. A notifications panel that expands and collapses smoothly, fades its content in step with the height change, and tells assistive technology exactly what the button controls.

Notifications3 unread
  • Deploy finishedproduction · 2 min ago
  • New comment on your PRreview requested · 1 hr ago
  • Weekly digest readyreports · yesterday

1. Install

One package, one peer dependency (React 18+). Other package managers and browser support notes are on the installation page.

SHELL
npm install @westopp/react-collapsible

Two imports and you're set: Collapsible is a named export, and the stylesheet — which carries the entire animation — is imported once at your app root.

TSXmain.tsx
// once, at your app root
import '@westopp/react-collapsible/styles.css'

// wherever you build the panel
import { Collapsible } from '@westopp/react-collapsible'

2. A minimal toggle

Collapsible is fully controlled — it holds no open/closed state of its own. You keep the state (useState here, a store if you prefer) and pass it down as open.

The other required prop is id, and it isn't decoration. The button that toggles the panel declares aria-controls with the same value, plus aria-expanded reflecting the current state. That pairing is what tells screen readers the button and the panel belong together — wire it from the first line, it's two attributes.

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

export const NotificationsPanel = () => {
const [open, setOpen] = useState(false)

return (
  <section>
    <button
      type="button"
      aria-controls="getting-started-notifications"
      aria-expanded={open}
      onClick={() => setOpen((v) => !v)}
    >
      {open ? 'Hide notifications' : 'Show notifications'}
    </button>

    <Collapsible id="getting-started-notifications" open={open}>
      <ul>
        <li>Deploy finished</li>
        <li>New comment on your PR</li>
        <li>Weekly digest ready</li>
      </ul>
    </Collapsible>
  </section>
)
}

This already animates. The stylesheet transitions the panel between collapsed and expanded over 500ms — no measuring, no layout effects, pure CSS. The full contract behind id, including how collapsed content is treated, is on the accessibility page.

3. Polish the motion

The collapse handles the panel's size; the content deserves its own polish. Inside className, a class prefixed with open: applies only while the panel is open, closed: only while it's closed, and anything unprefixed always applies. The component flips these in sync with the collapse animation, so pair them with a transition utility and the fade tracks the height change exactly.

While you're at it, transitionDuration swaps the stylesheet's 500ms default for whatever suits the panel.

TSXNotificationsPanel.tsx
<Collapsible
id="getting-started-notifications"
open={open}
transitionDuration="300ms"
className="open:opacity-100 closed:opacity-0 transition-opacity duration-300"
>
{/* the same list as before */}
</Collapsible>

Now the panel collapses in 300ms while the list fades over the same 300ms — which is exactly what the demo at the top of this page does.

4. Start expanded

Some panels should begin open. A Collapsible mounted with open already true animates open on first paint — the browser sees it enter the page exactly like a toggle. When that entry animation is unwanted, initialize your state to true and add initialOpen: the first frame renders with a zero-second transition, then the configured duration takes over for every toggle after.

TSXNotificationsPanel.tsx
const [open, setOpen] = useState(true)

return (
<Collapsible id="getting-started-notifications" open={open} initialOpen>
  {/* content */}
</Collapsible>
)

initialOpen doesn't manage state — open still does that. It only suppresses the mount animation. How the open prop is scheduled internally — including the one-frame deferral that lets your conditional classes transition on open instead of snapping — is covered in controlled state.

Where next

  • How it works — the CSS Grid technique end to end: 0fr to 1fr tracks, @starting-style, and why no JavaScript ever measures your content.
  • Building an accordion — the most common composite pattern: several panels, one open at a time.