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.