Animated tab-panel stack that keeps all panels mounted in the DOM and switches between them using an opacity/visibility crossfade, preventing layout shift.
## Key Components
### `CrossfadePanels`
The main exported component. Renders all panels into a single CSS grid cell (`col-start-1 row-start-1`), stacking them so the container height is always `max(panel heights)`. The active panel fades in at `z-[1]`; inactive panels fade out and receive `inert`, `aria-hidden`, and `pointer-events-none` once settled.
### Interfaces
| Interface | Purpose |
|---|---|
| `CrossfadePanel` | `{ id, content }` — a single panel descriptor |
| `CrossfadePanelsProps` | Component props: `activeId`, `panels`, `durationMs`, `className`, `panelClassName` |
## Key Behaviors
- **Height lock** — CSS grid single-cell stacking prevents CLS on switch
- **Fade-then-hide** — `visibility` interpolates alongside `opacity` so the outgoing panel stays paintable until the fade completes
- **Fully dead when hidden** — `inert` + `aria-hidden` eliminates tab stops, screen-reader exposure, and click/hover leaks (WCAG compliant)
- **Motion respect** — `prefers-reduced-motion` collapses the crossfade to an instant swap via `motion-reduce:transition-none`
## Usage Example
```typescript
import { CrossfadePanels } from './crossfade-panels'
const panels = [
{ id: 'overview', content: },
{ id: 'metrics', content: },
]
function Dashboard() {
const [activeId, setActiveId] = React.useState('overview')
return (
<>
>
)
}
```
> **When to prefer this over ``:** Use `CrossfadePanels` when panels are cheap to keep mounted and seamless switching matters (strips, media rails, dashboards). Use `` for heavy admin surfaces that should unmount when hidden.