import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks'; import { Layout, GridLayout } from './index'; import * as LayoutStories from './Layout.stories';
A compound component system for building application shells with headers, footers, sidebars, and scrollable content areas. The Layout system handles overflow management, panel resizing, and smooth transitions automatically.
- Building application shells with sidebars and navigation panels
- Creating pages with headers, footers, and scrollable content
- Implementing resizable side panels for tools or details views
- Managing complex layouts with nested content areas
Supports Base properties.
Customizes the root Layout element.
Sub-elements:
Inner- The inner content wrapper that adjusts based on panel sizes
These properties allow direct style application: width, height, padding, gap, fill, border, flow.
| Property | Type | Default | Description |
|---|---|---|---|
| doNotOverflow | boolean |
false |
When true, applies overflow: hidden to the root element. By default, overflow is visible. |
| Property | Type | Description |
|---|---|---|
| innerRef | ForwardedRef<HTMLDivElement> |
Ref for the inner content element |
| innerProps | HTMLAttributes<HTMLDivElement> |
Props to spread on the Inner sub-element |
When isGrid is enabled, the inner content becomes a CSS grid:
| Property | Type | Description |
|---|---|---|
| isGrid | boolean |
Enable grid display mode |
| columns | string |
Grid template columns |
| rows | string |
Grid template rows |
| template | string |
Grid template shorthand |
A horizontal bar typically placed at the top for navigation and actions.
<Layout.Toolbar>
<Title level={4}>App Name</Title>
<Space>
<Button>Settings</Button>
<Button type="primary">Profile</Button>
</Space>
</Layout.Toolbar>| Property | Type | Default | Description |
|---|---|---|---|
| children | ReactNode |
- | Content split between left and right |
Sub-elements:
- No sub-elements, styles apply to container
A semantic page header with title, breadcrumbs, subtitle, and action areas.
<Layout.Header
title="Dashboard"
level={2}
breadcrumbs={[
['Home', '/'],
['Reports', '/reports'],
]}
subtitle="Overview of your metrics"
extra={<Button type="primary">Create</Button>}
/>| Property | Type | Default | Description |
|---|---|---|---|
| title | ReactNode |
- | Page/section title |
| level | 1-6 |
3 |
Heading level for accessibility |
| breadcrumbs | [label, href][] |
- | Navigation breadcrumbs |
| subtitle | ReactNode |
- | Text below the title |
| suffix | ReactNode |
- | Content next to the title |
| extra | ReactNode |
- | Content on the right side |
| onBack | () => void |
- | Callback for the back button. When provided, a back arrow button appears to the left of the title. |
Sub-elements:
Back- Back button container (visible whenonBackis provided)Breadcrumbs- Navigation path containerTitle- Main heading elementSuffix- Content adjacent to titleExtra- Right-aligned actionsSubtitle- Secondary text below title
Scrollable content area with automatic overflow handling and custom scrollbar styles.
<Layout.Content scrollbar="tiny">
{/* Scrollable content */}
</Layout.Content>| Property | Type | Default | Description |
|---|---|---|---|
| scrollbar | 'default' | 'thin' | 'tiny' | 'none' |
'thin' |
Scrollbar style |
| children | ReactNode |
- | Content to display |
| innerRef | ForwardedRef<HTMLDivElement> |
- | Ref for the inner content element |
| innerProps | HTMLAttributes<HTMLDivElement> |
- | Props to spread on the Inner sub-element |
Scrollbar types:
default- Browser default scrollbarthin- Thin scrollbar (CSS native)tiny- Custom hover-visible indicator (3px)none- Hidden scrollbar (still scrollable)
Sub-elements:
Inner- Scrollable inner containerScrollbarV- Vertical scroll indicator (tiny mode)ScrollbarH- Horizontal scroll indicator (tiny mode)
Horizontally centered content area with constrained width. Ideal for forms, articles, and focused content that shouldn't span the full width.
<Layout.Container>
<Title>Article Title</Title>
<Text>Content is centered horizontally with a max-width constraint.</Text>
</Layout.Container>| Property | Type | Default | Description |
|---|---|---|---|
| children | ReactNode |
- | Content to display |
| innerRef | ForwardedRef<HTMLDivElement> |
- | Ref for the inner content element |
| innerProps | HTMLAttributes<HTMLDivElement> |
- | Props to spread on the Inner sub-element |
Width constraints:
- Minimum:
40x(320px at default gap) - Default:
100% - Maximum:
120x(960px at default gap)
Sub-elements:
Inner- Constrained width inner container
Centered content area for both horizontal and vertical alignment. Extends Layout.Container with vertical centering and text alignment. Ideal for empty states, loading screens, and hero sections.
<Layout.Center>
<Title>No Results</Title>
<Text>Try adjusting your search criteria.</Text>
<Button type="primary">Clear Filters</Button>
</Layout.Center>| Property | Type | Default | Description |
|---|---|---|---|
| children | ReactNode |
- | Content to display |
Styling differences from Container:
- Content is centered both horizontally and vertically
- Text is center-aligned by default
Sub-elements:
Inner- Constrained width inner container withtext-align: center
Footer area with support for inverted content order (primary actions on right).
<Layout.Footer invertOrder>
<Button.Group>
<Button type="primary">Save</Button>
<Button>Cancel</Button>
</Button.Group>
<Text color="#dark-03">Draft saved</Text>
</Layout.Footer>| Property | Type | Default | Description |
|---|---|---|---|
| invertOrder | boolean |
false |
Reverse child order (primary on right) |
| children | ReactNode |
- | Footer content |
Collapsible side panel with resizing, transitions, and multiple rendering modes.
<Layout.Panel
side="left"
size={250}
isResizable
minSize={150}
maxSize={400}
hasTransition
isOpen={isPanelOpen}
onOpenChange={setIsPanelOpen}
onSizeChange={setSize}
>
<Layout.PanelHeader isClosable title="Navigation" onClose={() => setIsPanelOpen(false)} />
<Layout.Content>
{/* Panel content */}
</Layout.Content>
</Layout.Panel>| Property | Type | Default | Description |
|---|---|---|---|
| side | 'left' | 'right' | 'top' | 'bottom' |
'left' |
Panel position |
| mode | 'default' | 'sticky' | 'overlay' | 'dialog' |
'default' |
Panel rendering mode |
| size | number |
- | Controlled panel size (px) |
| defaultSize | number |
280 |
Initial uncontrolled size |
| minSize | number |
200 |
Minimum size constraint |
| maxSize | number |
- | Maximum size constraint |
| isResizable | boolean |
false |
Enable drag-to-resize |
| isOpen | boolean |
- | Controlled open state |
| defaultIsOpen | boolean |
true |
Initial open state |
| onOpenChange | (isOpen: boolean) => void |
- | Open state callback |
| onSizeChange | (size: number) => void |
- | Size change callback |
| sizeStorageKey | string |
- | localStorage key for persisting size |
| hasTransition | boolean |
false |
Enable slide animation |
| isDismissable | boolean |
true |
Allow dismissing overlay/dialog by click or Escape |
| overlayStyles | Styles |
- | Styles for the overlay backdrop in overlay mode |
| isDialogOpen | boolean |
- | Controlled dialog state (mode="dialog") |
| defaultIsDialogOpen | boolean |
false |
Initial dialog state |
| onDialogOpenChange | (isOpen: boolean) => void |
- | Dialog state callback |
| dialogProps | CubeDialogContainerProps |
- | Props for dialog mode |
| innerRef | ForwardedRef<HTMLDivElement> |
- | Ref for the inner content element |
| innerProps | HTMLAttributes<HTMLDivElement> |
- | Props to spread on the Inner sub-element |
Panel Modes:
| Mode | Size Propagation | Overlay | Description |
|---|---|---|---|
default |
Yes | No | Standard panel that pushes content aside |
sticky |
No | No | Panel floats over content without affecting layout |
overlay |
Yes | Yes | Panel with dismissable backdrop overlay |
dialog |
No | Via Dialog | Panel renders as a modal dialog |
Modifiers:
| Modifier | Type | Description |
|---|---|---|
| side | 'left' | 'right' | 'top' | 'bottom' |
Panel position |
| drag | boolean |
Currently being resized |
| horizontal | boolean |
Left or right panel |
| has-transition | boolean |
Transition enabled |
| offscreen | boolean |
Panel is collapsed |
Resizable inline section within a layout. Unlike Layout.Panel (which is absolutely positioned), Pane participates in the normal flex/grid flow and can be resized via drag handles.
<Layout.Pane
isResizable
resizeEdge="right"
size={250}
minSize={150}
maxSize={400}
onSizeChange={setSize}
>
<Title level={5}>Pane Content</Title>
<Text>This pane can be resized by dragging the edge.</Text>
</Layout.Pane>| Property | Type | Default | Description |
|---|---|---|---|
| resizeEdge | 'right' | 'bottom' |
'right' |
Edge where resize handler appears |
| isResizable | boolean |
false |
Enable resize functionality |
| size | number | string |
- | Controlled size (width for right edge, height for bottom edge) |
| defaultSize | number | string |
200 |
Default size for uncontrolled state |
| minSize | number | string |
- | Minimum size constraint |
| maxSize | number | string |
- | Maximum size constraint |
| onSizeChange | (size: number) => void |
- | Size change callback |
| scrollbar | ScrollbarType |
'thin' |
Scrollbar style |
| children | ReactNode |
- | Content to display |
| innerRef | ForwardedRef<HTMLDivElement> |
- | Ref for the inner content element |
| innerProps | HTMLAttributes<HTMLDivElement> |
- | Props to spread on the Inner sub-element |
Sub-elements:
Inner- Scrollable inner containerScrollbarV- Vertical scroll indicator (tiny mode)ScrollbarH- Horizontal scroll indicator (tiny mode)ResizeHandler- Drag handle for resizingDrag- Visual drag indicatorDragPart- Individual dots in the drag indicator
Header for panels with optional close button.
<Layout.PanelHeader
title="Details"
isClosable
onClose={() => setOpen(false)}
/>| Property | Type | Default | Description |
|---|---|---|---|
| title | ReactNode |
- | Panel title |
| level | 1-6 |
2 |
Heading level |
| isClosable | boolean |
false |
Show close button |
| onClose | () => void |
- | Close button handler |
| actions | ReactNode |
- | Custom actions (replaces close) |
A variant of Layout that enables CSS Grid for the content area.
<GridLayout
height="100dvh"
columns="repeat(2, 1fr)"
rows="auto 1fr"
gap="2x"
padding="2x"
>
<Layout.Header title="Grid Dashboard" />
<div style={{ gridColumn: '1 / -1' }}>Full-width card</div>
<div>Left card</div>
<div>Right card</div>
</GridLayout>| Property | Type | Description |
|---|---|---|
| columns | string |
Grid template columns |
| rows | string |
Grid template rows |
| template | string |
Grid template shorthand |
| gap | string |
Grid gap |
<Layout height="100dvh">
<Layout.Header title="Dashboard" subtitle="Overview of your data" />
<Layout.Content>
<Text>Main content area</Text>
</Layout.Content>
<Layout.Footer>
<Text preset="t4" color="#dark-03">© 2024 Company</Text>
</Layout.Footer>
</Layout><Layout height="100dvh">
<Layout.Header
title="Analytics"
breadcrumbs={[
['Home', '/'],
['Reports', '/reports'],
]}
extra={
<Button.Group>
<Button>Export</Button>
<Button type="primary">New Report</Button>
</Button.Group>
}
/>
<Layout.Content>
<Text>Analytics content</Text>
</Layout.Content>
</Layout><Layout height="100dvh">
<Layout.Header
title="Report Details"
onBack={() => navigate(-1)}
extra={
<Space>
<Button>Export</Button>
<Button type="primary">Edit</Button>
</Space>
}
/>
<Layout.Content>
<Text>Page content with a back button in the header</Text>
</Layout.Content>
</Layout>function ResizablePanelExample() {
const [size, setSize] = useState(250);
return (
<Layout height="100dvh">
<Layout.Panel
isResizable
side="left"
size={size}
minSize={150}
maxSize={400}
onSizeChange={setSize}
>
<Layout.PanelHeader title="Resizable" />
<Layout.Content>
<Text>Drag the edge to resize (current: {size}px)</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Content>
<Text>Main content area</Text>
</Layout.Content>
</Layout>
);
}Use sizeStorageKey to persist panel size to localStorage. The size is automatically restored on mount.
<Layout height="100dvh">
<Layout.Panel
isResizable
side="left"
sizeStorageKey="app-sidebar-size"
defaultSize={250}
minSize={150}
maxSize={400}
>
<Layout.PanelHeader title="Persistent" />
<Layout.Content>
<Text>This panel remembers its size</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Content>
<Text>Main content area</Text>
</Layout.Content>
</Layout>function PanelWithTransitionExample() {
const [isPanelOpen, setIsPanelOpen] = useState(true);
return (
<Layout height="100dvh">
<Layout.Panel
hasTransition
side="right"
size={250}
isOpen={isPanelOpen}
onOpenChange={setIsPanelOpen}
>
<Layout.PanelHeader
isClosable
title="Details"
onClose={() => setIsPanelOpen(false)}
/>
<Layout.Content>
<Text>Panel content with slide transition</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Toolbar>
<Button onPress={() => setIsPanelOpen(!isPanelOpen)}>
{isPanelOpen ? 'Close' : 'Open'} Panel
</Button>
</Layout.Toolbar>
<Layout.Content>
<Text>Main content area</Text>
</Layout.Content>
</Layout>
);
}function MultiplePanelsExample() {
const [leftOpen, setLeftOpen] = useState(true);
const [rightOpen, setRightOpen] = useState(true);
return (
<Layout height="100dvh">
<Layout.Panel hasTransition side="left" size={180} isOpen={leftOpen}>
<Layout.PanelHeader isClosable title="Left" onClose={() => setLeftOpen(false)} />
<Layout.Content>
<Text>Left panel</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Panel hasTransition side="right" size={180} isOpen={rightOpen}>
<Layout.PanelHeader isClosable title="Right" onClose={() => setRightOpen(false)} />
<Layout.Content>
<Text>Right panel</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Toolbar>
<Space>
<Button onPress={() => setLeftOpen(!leftOpen)}>Left</Button>
<Button onPress={() => setRightOpen(!rightOpen)}>Right</Button>
</Space>
</Layout.Toolbar>
<Layout.Content>
<Text>Main content between two panels</Text>
</Layout.Content>
</Layout>
);
}In sticky mode, the panel floats over the content without pushing it aside. The main content area remains unaffected by the panel's size.
function StickyPanelExample() {
const [isPanelOpen, setIsPanelOpen] = useState(true);
return (
<Layout height="100dvh">
<Layout.Panel
hasTransition
mode="sticky"
side="left"
size={260}
isOpen={isPanelOpen}
onOpenChange={setIsPanelOpen}
>
<Layout.PanelHeader isClosable title="Sticky Panel" onClose={() => setIsPanelOpen(false)} />
<Layout.Content>
<Text>This panel floats over the content.</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Content>
<Text>Main content stays in place.</Text>
</Layout.Content>
</Layout>
);
}In overlay mode, a semi-transparent backdrop appears behind the panel. The panel is dismissed (when isDismissable is true, which is the default) when:
- Clicking the backdrop overlay
- Pressing Escape anywhere in the Layout
- Moving focus to the main content area
function OverlayPanelExample() {
const [isPanelOpen, setIsPanelOpen] = useState(true);
return (
<Layout height="100dvh">
<Layout.Panel
hasTransition
mode="overlay"
side="right"
size={300}
isOpen={isPanelOpen}
onOpenChange={setIsPanelOpen}
>
<Layout.PanelHeader isClosable title="Overlay Panel" onClose={() => setIsPanelOpen(false)} />
<Layout.Content>
<Text>Click the overlay, press Escape, or focus the main content to close.</Text>
</Layout.Content>
</Layout.Panel>
<Layout.Content>
<Text>Background content is dimmed.</Text>
</Layout.Content>
</Layout>
);
}You can customize the overlay styles using the overlayStyles prop:
<Layout.Panel mode="overlay" overlayStyles={{ fill: '#dark.5' }}>
{/* Panel with darker overlay */}
</Layout.Panel>To prevent dismissing the overlay by clicking or pressing Escape, set isDismissable={false}:
<Layout.Panel mode="overlay" isDismissable={false}>
{/* User must use explicit controls to close */}
</Layout.Panel><Layout height="100dvh">
<Layout.Header title="Tiny Scrollbar" />
<Layout.Content scrollbar="tiny">
{Array.from({ length: 20 }, (_, i) => (
<div key={i} style={{ padding: '8px' }}>
<Text>Line {i + 1}: Hover to reveal scrollbar indicator.</Text>
</div>
))}
</Layout.Content>
</Layout><Layout height="100dvh">
<Layout.Header title="Nested Layouts" />
<Layout flow="row">
<Layout width="200px" border="right">
<Layout.Content>
<Text>Left sidebar</Text>
</Layout.Content>
</Layout>
<Layout>
<Layout.Toolbar>
<Text>Inner toolbar</Text>
</Layout.Toolbar>
<Layout.Content>
<Text>Main content</Text>
</Layout.Content>
</Layout>
</Layout>
</Layout>function ApplicationShell() {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<Layout height="100dvh">
<Layout.Panel
hasTransition
isResizable
side="right"
defaultSize={240}
isOpen={sidebarOpen}
minSize={180}
maxSize={350}
onOpenChange={setSidebarOpen}
>
<Layout.PanelHeader
isClosable
title="Navigation"
onClose={() => setSidebarOpen(false)}
/>
<Layout.Content padding=".5x" scrollbar="tiny" gap="1bw">
{['Dashboard', 'Analytics', 'Reports'].map((item) => (
<ItemButton key={item} type="neutral" width="100%">
{item}
</ItemButton>
))}
</Layout.Content>
</Layout.Panel>
<Layout.Header
title="Dashboard"
level={2}
breadcrumbs={[['Home', '/'], ['Analytics', '/analytics']]}
subtitle="Real-time overview of your metrics"
suffix={
<Button type="neutral" onPress={() => setSidebarOpen(!sidebarOpen)}>
☰
</Button>
}
extra={
<Button.Group>
<Button>Export</Button>
<Button type="primary">Create Report</Button>
</Button.Group>
}
/>
<Layout.Content scrollbar="tiny">
{/* Main content */}
</Layout.Content>
<Layout.Footer>
<Text preset="t4" color="#dark-03">© 2024 Your Company</Text>
<Space>
<Button type="link" size="small">Privacy</Button>
<Button type="link" size="small">Terms</Button>
</Space>
</Layout.Footer>
</Layout>
);
}Tab- Navigate between interactive elements- Focus management is handled automatically when panels open/close
- Resize handlers can be focused and resized using arrow keys:
Arrow keys- Resize panel by 10pxShift + Arrow keys- Resize panel by 50pxDouble-clickon resize handler - Reset to default size
Layout.Headeruses semantic heading elements (h1-h6) based onlevelpropLayout.Toolbarhasrole="toolbar"for proper announcementLayout.Footerhasrole="contentinfo"for landmark navigation- Breadcrumbs use Link components with proper navigation semantics
- Panel close buttons have accessible labels
aria-label- Available on all components for custom labels- Landmarks are automatically set for header, footer, and toolbar
- Panel close buttons include
aria-label="Close panel" - Resize handlers have
role="separator"with properaria-orientationandaria-label
- Do: Set an explicit height on the root Layout
<Layout height="100dvh">
{/* Content won't overflow */}
</Layout>- Don't: Nest multiple Layout.Content without explicit heights
{/* This may cause layout issues */}
<Layout.Content>
<Layout.Content>...</Layout.Content>
</Layout.Content>- Do: Use appropriate heading levels for accessibility
<Layout.Header title="Page Title" level={1} />- Do: Provide close handlers when panels are closable
<Layout.Panel isOpen={isOpen} onOpenChange={setIsOpen}>
<Layout.PanelHeader
isClosable
title="Panel"
onClose={() => setIsOpen(false)}
/>
</Layout.Panel>- Do: Use
hasTransitionfor better UX when toggling panels
<Layout.Panel hasTransition side="left" isOpen={isOpen}>
{/* Smooth slide animation */}
</Layout.Panel>Layout uses a two-element architecture (wrapper + content). The wrapper has overflow: visible by default, allowing content to be visible outside its boundaries. Use doNotOverflow prop to apply overflow: hidden when you need to clip content. The inner element has absolute positioning with insets that adjust based on panel sizes.
Panels automatically register with the Layout context and report their sizes. This allows the main content area to adjust its insets accordingly, preventing content from being hidden behind panels.
Each Layout.Content and panel header uses LayoutContextReset internally, which resets the layout context. This prevents nested Layouts from accidentally affecting parent layouts.
Panel transitions are disabled during drag operations and initial mount to prevent visual glitches. The isReady state ensures transitions only activate after the first paint.
- Collapsible mode: Add a mode where panels collapse to icons/mini state instead of fully hiding
- Responsive breakpoints: Automatically switch panels to dialog mode on smaller screens
- Open state persistence: Add
openStorageKeyfor persisting panel open/closed state (size persistence is available viasizeStorageKey) - Split view: Support for multiple resizable content areas (not just side panels)
- Layout Guide - Patterns and examples for building layouts
- Space - For simpler flex layouts
- Dialog - For modal panels
- Item - Used by PanelHeader