Skip to content

Panel

Github icon-storybook-square Storybook Markdown Copy for LLM

A responsive, composable side panel layout for navigation, filters, or supporting content.

Import

import { Panel } from "@cfa/react-core";

Guidelines

When to use

  • Panel is a persistent side column beside the main content. It is part of the page layout, not an overlay.

Do

  • Hold one coherent purpose in the side column.

Avoid

  • Using Panel for temporary or task-scoped content that overlays the page. Use Drawer.
  • Using Panel for a blocking decision. Use Modal.
  • Using Panel for content in the main content area. Use Card or the main layout column.

Examples

Full Page

The standard Panel layout uses full-page positioning and should be used for application-level layouts. The docs site cannot render this version as a live example because the panel is positioned relative to the full viewport instead of the preview frame. Refer to Storybook for a working interactive example.

<Panel.Provider>
<Panel.Root aria-label="Application navigation">
<Panel.Header>
<span>Application</span>
</Panel.Header>
<Panel.Separator />
<Panel.Content>
<a href="/dashboard">Dashboard</a>
<a href="/projects">Projects</a>
<a href="/settings">Settings</a>
</Panel.Content>
<Panel.Separator />
<Panel.Footer>
<span>John Doe</span>
</Panel.Footer>
</Panel.Root>
<Panel.Inset>
<div style={{ padding: 16 }}>
<Panel.Trigger />
<h1>Main Content</h1>
<p>
The panel can be toggled with the trigger button or the default Cmd+B / Ctrl+B keyboard
shortcut.
</p>
</div>
</Panel.Inset>
</Panel.Provider>

Controlled

Use open and onOpenChange when the parent component needs to own the panel’s open state.

function ControlledPanelExample() {
const [open, setOpen] = React.useState(true);
return (
<Panel.Provider open={open} onOpenChange={setOpen}>
<Panel.Root aria-label="Controlled panel">
<Panel.Header>
<span>Controlled Panel</span>
</Panel.Header>
<Panel.Content>
<p>Panel is {open ? "open" : "closed"}.</p>
</Panel.Content>
</Panel.Root>
<Panel.Inset>
<div style={{ padding: 16 }}>
<Panel.Trigger />
</div>
</Panel.Inset>
</Panel.Provider>
);
}

Common Options

Use side="right" to position the panel on the right side. Use defaultOpen={false} for an uncontrolled panel that starts closed. The default keyboard shortcut is Cmd+B on Mac and Ctrl+B on Windows or Linux; set keyboardShortcut to customize the key or disableKeyboardShortcut to turn the shortcut off.

<Panel.Provider defaultOpen={false} side="right" keyboardShortcut="k">
{/* Panel.Root and Panel.Inset */}
</Panel.Provider>

Contained Layout

The default viewport layout is preferred for application shells. Use layout="contained" for bounded regions such as dashboard panels, split panes, or documentation previews.

Panel.Inset renders a main landmark by default because most Panel usage represents an application shell. When a contained Panel is rendered inside an existing page, use as="section" or as="div" to keep the inset layout behavior without creating another page-level main landmark.

<div style={{ height: 400, border: "1px solid #ccc", overflow: "hidden" }}>
<Panel.Provider layout="contained" disableKeyboardShortcut>
<Panel.Root aria-label="Contained navigation">
<Panel.Header>
<span>Panel</span>
</Panel.Header>
<Panel.Content>
<a href="/option-1">Option 1</a>
<a href="/option-2">Option 2</a>
<a href="/option-3">Option 3</a>
</Panel.Content>
</Panel.Root>
<Panel.Inset as="section" aria-label="Contained panel content">
<div style={{ padding: 16 }}>
<Panel.Trigger />
<h2>Contained Panel</h2>
<p>This content is scoped to the bounded container.</p>
</div>
</Panel.Inset>
</Panel.Provider>
</div>

If a contained Panel is nested inside an application shell that already owns the keyboard shortcut, set disableKeyboardShortcut on the nested Panel.Provider to avoid competing Cmd+B or Ctrl+B handlers.