Components

Shell

The Shell component is a complete layout engine for building application interfaces. It manages layout state, composition rules, and responsive behavior across seven core slots.View source →

The Shell component is a complete layout engine for building application interfaces. It manages layout state, composition rules, and responsive behavior across seven core slots: Header, Rail, Panel, Sidebar, Content, Inspector, and Bottom.

Installation

shell
npm install @kushagradhawan/kookie-ui

Usage

tsx
import { Shell } from '@kushagradhawan/kookie-ui';
 
export function MyApp() {
  return (
    <Shell.Root>
      <Shell.Sidebar defaultState="expanded">
        <nav>Navigation</nav>
      </Shell.Sidebar>
      <Shell.Content>
        <main>Main content</main>
      </Shell.Content>
    </Shell.Root>
  );
}

Anatomy

Shell is a compound component with the following parts:

tsx
<Shell.Root>
  <Shell.Header>{/* Global navigation */}</Shell.Header>
  <Shell.Rail>{/* Icon strip */}</Shell.Rail>
  <Shell.Panel>{/* Expanded navigation */}</Shell.Panel>
  <Shell.Sidebar>{/* Unified sidebar (alternative to Rail+Panel) */}</Shell.Sidebar>
  <Shell.Content>{/* Main content area (required) */}</Shell.Content>
  <Shell.Inspector>{/* Right-side panel */}</Shell.Inspector>
  <Shell.Bottom>{/* Bottom panel */}</Shell.Bottom>
  <Shell.Trigger target="sidebar">{/* Toggle button */}</Shell.Trigger>
</Shell.Root>

Root Props

The Root component manages the overall shell layout and provides context to child components.

PropTypeDescription
height'full' | 'auto' | string | numberHeight of the shell container. Default: 'full'
childrenReact.ReactNodeShell components (Header, Sidebar, Content, etc.)

Header Props

Fixed header at the top of the shell for global navigation and branding.

PropTypeDescription
heightnumberHeader height in pixels. Default: 64
childrenReact.ReactNodeHeader content

Sidebar Props

Unified navigation sidebar with three states: collapsed, thin, and expanded. Cannot coexist with Rail or Panel.

PropTypeDescription
state'collapsed' | 'thin' | 'expanded' | Responsive<SidebarMode>Controlled sidebar state
defaultState'collapsed' | 'thin' | 'expanded' | Responsive<SidebarMode>Initial sidebar state. Default: 'expanded'
onStateChange(state: SidebarMode, meta: { reason: 'init' | 'toggle' | 'responsive' }) => voidCallback fired when state changes
presentation'fixed' | 'overlay' | 'stacked' | ResponsivePresentationHow sidebar interacts with layout. Default: { initial: 'overlay', md: 'fixed' }
expandedSizenumberWidth when expanded in pixels. Default: 288
thinSizenumberWidth when thin in pixels. Default: 64
toggleModes'both' | 'single'Toggle sequence: 'both' cycles through all states, 'single' toggles between collapsed and expanded
sizenumber | stringControlled size (width when expanded)
defaultSizenumber | stringInitial size
onSizeChange(size: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => voidCallback fired when size changes
sizeUpdate'throttle' | 'debounce'Strategy for size change callbacks
sizeUpdateMsnumberMilliseconds for throttle/debounce. Default: 50
resizablebooleanEnable drag-to-resize. Default: false
collapsiblebooleanAllow collapsing. Default: true
minSizenumberMinimum width in pixels. Default: 200
maxSizenumberMaximum width in pixels. Default: 400
onExpand() => voidCallback fired when sidebar expands
onCollapse() => voidCallback fired when sidebar collapses
onResize(size: number) => voidCallback fired during resize
onResizeStart(size: number) => voidCallback fired when resize starts
onResizeEnd(size: number) => voidCallback fired when resize ends
snapPointsnumber[]Array of sizes to snap to during resize
snapTolerancenumberDistance in pixels to trigger snap
collapseThresholdnumberSize below which sidebar auto-collapses
paneIdstringUnique identifier for persistence
persistencePaneSizePersistenceAdapter for saving/loading size
insetbooleanWhen true, adds margin and triggers gray backdrop on Shell
childrenReact.ReactNodeSidebar content

Rail Props

Slim navigation strip (icon bar). Use with Panel for a two-part navigation layout. Cannot coexist with Sidebar.

PropTypeDescription
openboolean | Responsive<boolean>Controlled open state
defaultOpenboolean | Responsive<boolean>Initial open state. Supports responsive objects. Default: true
onOpenChange(open: boolean, meta: { reason: 'init' | 'toggle' | 'responsive' | 'panel' }) => voidCallback fired when open state changes
presentation'fixed' | 'overlay' | 'stacked' | ResponsivePresentationHow rail interacts with layout. Default: { initial: 'fixed', sm: 'fixed' }
expandedSizenumberWidth in pixels. Default: 64
collapsiblebooleanAllow collapsing. Default: true
onExpand() => voidCallback fired when rail expands
onCollapse() => voidCallback fired when rail collapses
insetbooleanWhen true, adds margin to Rail+Panel and triggers gray backdrop on Shell
childrenReact.ReactNodeRail content (typically icons)

Panel Props

Expanded navigation panel that works alongside Rail. Collapses when Rail collapses. Cannot coexist with Sidebar.

PropTypeDescription
openboolean | Responsive<boolean>Controlled open state
defaultOpenboolean | Responsive<boolean>Initial open state. Supports responsive objects. Default: false
onOpenChange(open: boolean, meta: { reason: 'init' | 'toggle' | 'left' | 'responsive' }) => voidCallback fired when open state changes
expandedSizenumberWidth when expanded in pixels. Default: 288
sizenumber | stringControlled size (width)
defaultSizenumber | stringInitial size
onSizeChange(size: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => voidCallback fired when size changes
sizeUpdate'throttle' | 'debounce'Strategy for size change callbacks
sizeUpdateMsnumberMilliseconds for throttle/debounce. Default: 50
minSizenumberMinimum width in pixels. Default: 100
maxSizenumberMaximum width in pixels. Default: 800
resizablebooleanEnable drag-to-resize. Default: false
collapsiblebooleanAllow collapsing. Default: true
onExpand() => voidCallback fired when panel expands
onCollapse() => voidCallback fired when panel collapses
onResize(size: number) => voidCallback fired during resize
onResizeStart(size: number) => voidCallback fired when resize starts
onResizeEnd(size: number) => voidCallback fired when resize ends
snapPointsnumber[]Array of sizes to snap to during resize
snapTolerancenumberDistance in pixels to trigger snap
collapseThresholdnumberSize below which panel auto-collapses
paneIdstringUnique identifier for persistence
persistencePaneSizePersistenceAdapter for saving/loading size
insetbooleanWhen true, adds margin to Rail+Panel and triggers gray backdrop on Shell
childrenReact.ReactNodePanel content

Content Props

Main content area. Always required in shell layouts.

PropTypeDescription
insetbooleanWhen true, adds margin and triggers gray backdrop on Shell
childrenReact.ReactNodeContent to display

Inspector Props

Right-side panel for inspectors, property panels, or contextual information.

PropTypeDescription
openboolean | Responsive<boolean>Controlled open state
defaultOpenboolean | Responsive<boolean>Initial open state. Default: false
onOpenChange(open: boolean, meta: { reason: 'init' | 'toggle' | 'responsive' }) => voidCallback fired when open state changes
presentation'fixed' | 'overlay' | 'stacked' | ResponsivePresentationHow inspector interacts with layout. Default: { initial: 'overlay', lg: 'fixed' }
sizenumber | stringControlled size (width when expanded)
defaultSizenumber | stringInitial size
onSizeChange(size: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => voidCallback fired when size changes
sizeUpdate'throttle' | 'debounce'Strategy for size change callbacks
sizeUpdateMsnumberMilliseconds for throttle/debounce. Default: 50
expandedSizenumberWidth when expanded in pixels. Default: 320
minSizenumberMinimum width in pixels. Default: 200
maxSizenumberMaximum width in pixels. Default: 500
resizablebooleanEnable drag-to-resize. Default: false
collapsiblebooleanAllow collapsing. Default: true
onExpand() => voidCallback fired when inspector expands
onCollapse() => voidCallback fired when inspector collapses
onResize(size: number) => voidCallback fired during resize
onResizeStart(size: number) => voidCallback fired when resize starts
onResizeEnd(size: number) => voidCallback fired when resize ends
snapPointsnumber[]Array of sizes to snap to during resize
snapTolerancenumberDistance in pixels to trigger snap
collapseThresholdnumberSize below which inspector auto-collapses
paneIdstringUnique identifier for persistence
persistencePaneSizePersistenceAdapter for saving/loading size
insetbooleanWhen true, adds margin and triggers gray backdrop on Shell
childrenReact.ReactNodeInspector content

Bottom Props

Bottom panel for terminals, logs, or contextual information.

PropTypeDescription
openboolean | Responsive<boolean>Controlled open state
defaultOpenboolean | Responsive<boolean>Initial open state. Default: false
onOpenChange(open: boolean, meta: { reason: 'init' | 'toggle' | 'responsive' }) => voidCallback fired when open state changes
presentation'fixed' | 'overlay' | 'stacked' | ResponsivePresentationHow bottom panel interacts with layout. Default: 'fixed'
sizenumber | stringControlled size (height when expanded)
defaultSizenumber | stringInitial size
onSizeChange(size: number, meta: { reason: 'init' | 'resize' | 'controlled' }) => voidCallback fired when size changes
sizeUpdate'throttle' | 'debounce'Strategy for size change callbacks
sizeUpdateMsnumberMilliseconds for throttle/debounce. Default: 50
expandedSizenumberHeight when expanded in pixels. Default: 200
minSizenumberMinimum height in pixels. Default: 100
maxSizenumberMaximum height in pixels. Default: 400
resizablebooleanEnable drag-to-resize. Default: false
collapsiblebooleanAllow collapsing. Default: true
onExpand() => voidCallback fired when bottom panel expands
onCollapse() => voidCallback fired when bottom panel collapses
onResize(size: number) => voidCallback fired during resize
onResizeStart(size: number) => voidCallback fired when resize starts
onResizeEnd(size: number) => voidCallback fired when resize ends
snapPointsnumber[]Array of sizes to snap to during resize
snapTolerancenumberDistance in pixels to trigger snap
collapseThresholdnumberSize below which bottom panel auto-collapses
paneIdstringUnique identifier for persistence
persistencePaneSizePersistenceAdapter for saving/loading size
insetbooleanWhen true, adds margin and triggers gray backdrop on Shell
childrenReact.ReactNodeBottom panel content

Trigger Props

Button trigger for controlling shell panes. Renders as an unstyled button—wrap with IconButton for styling.

PropTypeDescription
target'sidebar' | 'inspector' | 'bottom' | 'rail' | 'panel'Which pane to control
action'toggle' | 'expand' | 'collapse'Action to perform. Default: 'toggle'
peekOnHoverbooleanTemporarily show pane on hover without changing state. Default: false
childrenReact.ReactNodeTrigger content (typically an icon)

Examples

Basic Sidebar Layout

The most common layout pattern with a collapsible sidebar.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded" presentation={{ initial: 'overlay', md: 'fixed' }}>
    <nav>Navigation content</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <main>Main application content</main>
  </Shell.Content>
</Shell.Root>

With Header

Add a fixed header for global navigation.

tsx
<Shell.Root>
  <Shell.Header height={56}>
    <Flex align="center" justify="between" px="4" height="100%">
      <span>Logo</span>
      <nav>Header actions</nav>
    </Flex>
  </Shell.Header>
  <Shell.Sidebar defaultState="expanded">
    <nav>Sidebar</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <main>Content</main>
  </Shell.Content>
</Shell.Root>

With Inspector

Add a right-side panel for properties or details.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded">
    <nav>Navigation</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <main>Content</main>
  </Shell.Content>
  <Shell.Inspector defaultOpen expandedSize={320}>
    <aside>Inspector panel</aside>
  </Shell.Inspector>
</Shell.Root>

With Bottom Panel

Add a bottom panel for terminals or logs.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded">
    <nav>Navigation</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <main>Content</main>
  </Shell.Content>
  <Shell.Bottom defaultOpen expandedSize={200}>
    <div>Terminal output</div>
  </Shell.Bottom>
</Shell.Root>

Rail + Panel Layout

Use Rail and Panel for a two-part navigation with icon strip and expanded panel.

tsx
<Shell.Root>
  <Shell.Rail defaultOpen expandedSize={64}>
    <nav>Icon strip</nav>
  </Shell.Rail>
  <Shell.Panel defaultOpen expandedSize={280}>
    <nav>Expanded navigation</nav>
  </Shell.Panel>
  <Shell.Content>
    <main>Content</main>
  </Shell.Content>
</Shell.Root>

Inset Layout

Use inset prop to create floating panes with a gray backdrop.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded" inset>
    <Sidebar variant="surface">
      <nav>Floating navigation</nav>
    </Sidebar>
  </Shell.Sidebar>
  <Shell.Content inset>
    <main>Floating content</main>
  </Shell.Content>
  <Shell.Inspector defaultOpen inset>
    <aside>Floating inspector</aside>
  </Shell.Inspector>
</Shell.Root>

Controlled Sidebar

Control the sidebar state programmatically.

tsx
function App() {
  const [sidebarState, setSidebarState] = React.useState<'collapsed' | 'thin' | 'expanded'>('expanded');
 
  return (
    <Shell.Root>
      <Shell.Sidebar state={sidebarState} onStateChange={setSidebarState} toggleModes="single">
        <nav>Navigation</nav>
      </Shell.Sidebar>
      <Shell.Content>
        <Button onClick={() => setSidebarState('collapsed')}>Collapse</Button>
        <Button onClick={() => setSidebarState('expanded')}>Expand</Button>
      </Shell.Content>
    </Shell.Root>
  );
}

Resizable Sidebar

Enable drag-to-resize functionality.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded" resizable minSize={200} maxSize={500} expandedSize={300}>
    <nav>Resizable sidebar</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <main>Content</main>
  </Shell.Content>
</Shell.Root>

Toggle Trigger

Use Shell.Trigger to create toggle buttons for panes.

tsx
<Shell.Root>
  <Shell.Sidebar defaultState="expanded">
    <nav>Navigation</nav>
  </Shell.Sidebar>
  <Shell.Content>
    <IconButton asChild variant="ghost">
      <Shell.Trigger target="sidebar">
        <MenuIcon />
      </Shell.Trigger>
    </IconButton>
    <main>Content</main>
  </Shell.Content>
</Shell.Root>

Presentation Modes

The presentation prop controls how panes interact with the layout. Supports responsive values.

Fixed

Pane takes space in the layout flow. Content adjusts around it.

tsx
<Shell.Sidebar presentation="fixed" defaultState="expanded">
  <nav>Fixed sidebar</nav>
</Shell.Sidebar>

Overlay

Pane floats above content as a modal sheet. Common for mobile layouts.

tsx
<Shell.Sidebar presentation="overlay" defaultState="collapsed">
  <nav>Overlay sidebar</nav>
</Shell.Sidebar>

Stacked

Pane positions above content without displacing it. For floating panels.

tsx
<Shell.Sidebar presentation="stacked" defaultState="expanded">
  <nav>Stacked sidebar</nav>
</Shell.Sidebar>

Responsive Presentation

Change presentation based on viewport size.

tsx
<Shell.Sidebar presentation={{ initial: 'overlay', md: 'fixed' }} defaultState={{ initial: 'collapsed', md: 'expanded' }}>
  <nav>Responsive sidebar</nav>
</Shell.Sidebar>

Sidebar States

The sidebar supports three states: collapsed, thin, and expanded.

Collapsed

Sidebar is hidden. Use trigger buttons to expand.

tsx
<Shell.Sidebar defaultState="collapsed">
  <nav>Will start hidden</nav>
</Shell.Sidebar>

Thin

Sidebar shows at thinSize width (default 64px). Useful for icon-only navigation.

tsx
<Shell.Sidebar defaultState="thin" thinSize={72}>
  <nav>Icon navigation</nav>
</Shell.Sidebar>

Expanded

Sidebar shows at full expandedSize width (default 288px).

tsx
<Shell.Sidebar defaultState="expanded" expandedSize={320}>
  <nav>Full navigation</nav>
</Shell.Sidebar>

Toggle Modes

Control the toggle sequence with toggleModes.

tsx
{/* Cycles: collapsed → thin → expanded → collapsed */}
<Shell.Sidebar toggleModes="both" defaultState="collapsed">
  <nav>Three-state toggle</nav>
</Shell.Sidebar>
 
{/* Toggles: collapsed ↔ expanded (or thin if defaultState="thin") */}
<Shell.Sidebar toggleModes="single" defaultState="expanded">
  <nav>Two-state toggle</nav>
</Shell.Sidebar>

Responsive

Use responsive objects for breakpoint-specific behavior. Shell uses a mobile-first approach.

tsx
<Shell.Sidebar
  presentation={{ initial: 'overlay', sm: 'fixed' }}
  defaultState={{ initial: 'collapsed', sm: 'expanded' }}
>
  <nav>Responsive sidebar</nav>
</Shell.Sidebar>

Breakpoints

BreakpointValueDescription
initial-Base styles (mobile-first)
xs520pxExtra small screens
sm768pxSmall screens (tablets)
md1024pxMedium screens (laptops)
lg1280pxLarge screens (desktops)
xl1640pxExtra large screens

Inset Mode

The inset prop creates floating panes with visual separation from the shell background.

How It Works

When any pane has inset:

  1. Shell body gets a gray backdrop (var(--gray-2))
  2. The inset pane gets margin, creating gaps from edges and other panes
  3. The pane gets rounded corners for the floating appearance

Visual Treatment

The inset prop handles spatial positioning only. For visual styling (background, shadow, border), use the child component's variant prop:

tsx
<Shell.Sidebar inset>
  <Sidebar variant="surface">  {/* surface variant adds bg + shadow */}
    <nav>Navigation</nav>
  </Sidebar>
</Shell.Sidebar>
VariantBackgroundShadowBest for
surfaceSolidYesPrimary floating panels
softTintedSubtleSecondary panels
outlineTransparentNoBordered panels
ghostTransparentNoMinimal visual treatment

Customizing Gap

Override the inset gap with CSS variables:

css
.rt-ShellBody[data-has-inset] {
  --shell-inset-gap: var(--space-3);
}

Composition Rules

Shell enforces composition rules to prevent conflicting layouts.

Valid: Sidebar Only

Sidebar is a unified component that handles both icon strip and expanded navigation.

tsx
<Shell.Root>
  <Shell.Sidebar>...</Shell.Sidebar>
  <Shell.Content>...</Shell.Content>
</Shell.Root>

Valid: Rail + Panel

Rail provides an icon strip; Panel provides the expanded navigation. Panel collapses when Rail collapses.

tsx
<Shell.Root>
  <Shell.Rail>...</Shell.Rail>
  <Shell.Panel>...</Shell.Panel>
  <Shell.Content>...</Shell.Content>
</Shell.Root>

Invalid: Mixed

Sidebar cannot coexist with Rail or Panel. This triggers a console warning.

tsx
{/* ❌ Invalid */}
<Shell.Root>
  <Shell.Sidebar>...</Shell.Sidebar>
  <Shell.Rail>...</Shell.Rail>
  <Shell.Content>...</Shell.Content>
</Shell.Root>

Hooks

useShell

Access shell context for programmatic control.

tsx
import { useShell } from '@kushagradhawan/kookie-ui';
 
function MyComponent() {
  const shell = useShell();
 
  return (
    <Button onClick={() => shell.togglePane('sidebar')}>Toggle Sidebar</Button>
  );
}

Available methods:

  • togglePane(target) — Toggle a pane's state
  • expandPane(target) — Expand a pane
  • collapsePane(target) — Collapse a pane
  • sidebarMode — Current sidebar state
  • inspectorMode — Current inspector state
  • bottomMode — Current bottom panel state

Accessibility

Shell provides comprehensive accessibility through semantic HTML and ARIA attributes.

Semantic Structure

  • Shell.Header renders as <header>
  • Shell.Content renders as <main>
  • Proper landmark structure for screen readers

Keyboard Navigation

  • Tab navigation through all interactive elements
  • Escape key closes overlay presentations
  • Arrow keys for resize handle adjustment
  • Enter/Space for trigger activation

Screen Reader Support

  • State announcements for panel open/close
  • Resize operations announce current dimensions
  • ARIA attributes: aria-expanded, aria-hidden, aria-label

High Contrast

  • Enhanced focus indicators
  • Windows High Contrast mode compatibility
  • Proper contrast ratios for resize handles

Changelog

Added

  • Complete Shell component with seven core slots
  • Responsive presentation modes: fixed, overlay, stacked
  • Advanced resize system with keyboard navigation
  • Composition rule enforcement
  • Breakpoint system with mobile-first approach
  • Size persistence API
  • Peek functionality for temporary previews
  • useShell() hook for context access
  • ARIA slider implementation for resize handles
  • Focus management for overlay presentations
  • inset prop for floating pane layouts with gray backdrop

Technical

  • Context splitting for performance
  • TypeScript types for all configurations
  • Six breakpoints: initial, xs, sm, md, lg, xl
  • Mobile-first approach with overlay defaults
© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background