Corpora UI
Blocks

Shell

Inset application shell with a collapsible sidebar, workspace header, command-style search affordance, top actions, and scrollable content area.

Preview and props

Expand Props below the centered preview to change the component’s properties.

Examples

Use the panel triggers to open and close the sidebar and inspector.

Usage

import {
  ShellLayout,
  useShellPanels,
  useShellFitActions,
} from "@exegia/corpora-ui"

function App() {
  const panels = useShellPanels({
    shellId: "app-shell",
    onPanelChange: (open, side) =>
      console.log(side, open ? "expanded" : "collapsed"),
  })

  return (
    <ShellLayout
      {...panels.providerProps}
      variant="web"
      panels={{
        right: {
          id: "inspector",
          name: "Inspector",
          side: "right",
          open: false,
          component: <Inspector />,
        },
      }}
    >
      <Dashboard />
    </ShellLayout>
  )
}

// panels.openPanel("right", <Details item={item} />) — open the drawer with
// dynamic content; it replaces the static panels entry until the next openPanel.
// panels.toggle("right") from anywhere — a title-bar button, a shortcut.
// panels.isNarrow / panels.panelWidth mirror the shell's own measurement.
// Or by id, from any component under ExegiaProvider:
const inspector = useShellFitActions("app-shell")
inspector.resizePanel(480)

API notes

PropTypeDefaultDescription
childrenReact.ReactNode—Main scrollable content rendered inside the inset panel.
variant"web" | "desktop"—Desktop reserves room at the top for a native title bar; web does not.
panelsTPanelMap—Panel content keyed by side. left fills the collapsible icon rail, right fills the offcanvas drawer — it has no icon state, it is either fully shown or fully hidden. Each panel's own open/defaultOpen seeds its side's initial state, and its optional trigger node replaces the default icon inside that side's header trigger.
open / defaultOpen / onOpenChangePartial<Record<Side, boolean>> ×2 / (open, side) => void—Side-keyed control of the panels — no per-side props. ⌘B toggles the left rail. Spread useShellPanels().providerProps instead of wiring them by hand.
header / trailingReact.ReactNode ×2—header fills the middle of the shell's header row (breadcrumbs, search). trailing is pinned to the row's right edge, rendered just before the right panel's trigger — it stays flush right even when header is omitted or the panels are absent.
panelComponentsPartial<Record<Side, ReactNode>&gt—Dynamic content per side, normally filled by useShellPanels().openPanel(side, component) and delivered through providerProps. A side's entry wins over the static panels map's component, and sticks until the next openPanel replaces it — so one shell drawer can host whatever the app last opened into it.
shellId / defaultPanelWidthstring / number—The shell measures itself (rail + body + panel floors plus gaps and padding vs. its container) and files the verdict in Jotai atoms under shellId — whether the secondary panel fits, its px width, its resize bounds. Name the id to read it anywhere below ExegiaProvider with useShellFitState(id) / drive it with useShellFitActions(id), and to keep a dragged width across a route change. defaultPanelWidth seeds the panel's opening width instead of --panel-width. Column widths are CSS variables on the provider (--sidebar-width, --panel-width, --inset-min-width) — override them by restyling.

Panel controls from anywhere

Panel resizing and folds use motion-panels; the shell keeps its existing surfaces and icon rail. Drag the sidebar or inspector edge, use arrow keys (Shift for larger steps), press Enter to collapse, or double-click to reset its width.

Give the shell a stable shellId. Any component under the same ExegiaProvider can read and control that instance without being inside the shell:

const panels = useShellPanels({ shellId: "app-shell" })
panels.toggle("right")
panels.openPanel("right", <Details />)
panels.resizePanel(420)
panels.resizeSidebar(280)
// panels.open, panels.panelWidth, and panels.sidebarWidth reflect the shell.

Only spread providerProps at the shell's owning component. Remote consumers use the actions; they do not need another provider. Controlled open props still report changes through the owner's callbacks.

Responsive behavior

The shell measures its own container, including embedded previews and split views. It reserves the main content's preferred width, shrinks the expanded sidebar down to --sidebar-min-width (180px), then folds it to the icon rail. When even the icon rail would crowd the content, it hides the rail and disables its expansion control. The content can then fill very narrow screens.

The sidebar's requested width and open state are retained and restored when space returns. The inspector closes when it cannot fit alongside the content and sidebar. Read useShellPanels({ shellId }).sidebar or useShellFitState(shellId).sidebar for the effective width, mode (expanded, icon, or hidden), and canExpand flag.

Last updated on

On this page