A headless React component library designed for building IDE-like layouts (similar to VSCode).
- Sizing - Support both pixel (px) and ratio-based sizing modes
- Constraints - Support min/max pixel constraints
- Collapsible - Panels support collapse operations
- Maximize - Panels support maximize operations
- Persistence - Save and restore layout state
Build first
cd /path/to/resizable-panelspnpm install && pnpm buildIn your project's package.json add the following
{
"dependencies": {
"@local/resizable-panels": "link:/path/to/resizable-panels"
}
}Then install
cd /path/to/your-projectpnpm installThis project uses pnpm workspaces. To run the example:
pnpm installpnpm devIf you want to strip the console logs, run:
pnpm dev:minifyNote: Do not run pnpm install in the example directory.
Due to workspace configuration, it must be run from the project root.
import { ResizableContext, ResizableGroup, ResizablePanel, ResizableHandle } from '@local/resizable-panels';
function App() {
return (
<ResizableContext className="h-screen">
<ResizableGroup direction="col">
<ResizablePanel defaultSize={300} minSize={200}>
<div style={{ background: '#f0f0f0', height: '100%' }}>
Left Panel
</div>
</ResizablePanel>
<ResizablePanel defaultSize={500} minSize={300}>
<div style={{ background: '#e0e0e0', height: '100%' }}>
Right Panel
</div>
</ResizablePanel>
</ResizableGroup>
</ResizableContext>
);
}The root container that manages all resizable groups and handles global mouse events.
interface ResizableContextProps {
id?: string; // Unique identifier
children?: ReactNode; // Child elements
className?: string; // CSS class name
onContextMount?: (context: ContextValue) => void; // Context mount callback - for loading saved layout
onStateChanged?: (context: ContextValue) => void; // State change callback - for saving changed layout
}A container for grouping panels in the same direction.
interface ResizableGroupProps {
id?: string; // Unique identifier
children?: ReactNode; // Child elements (ResizablePanels)
className?: string; // CSS class name
direction?: 'row' | 'col'; // Resize direction (default: 'col')
// 'col' = panels arranged horizontally (left-right), drag handle resizes horizontally
// 'row' = panels arranged vertically (top-bottom), drag handle resizes vertically
ratio?: boolean; // Use ratio-based flex layout (default: false)
// When true, panel sizes are used as flex-grow ratio
}An individual resizable panel.
interface ResizablePanelProps {
id?: string; // Unique identifier
children?: ReactNode; // Panel content
className?: string; // CSS class name
expand?: boolean; // Grow/shrink when group size changes (default: false)
minSize?: number; // Minimum size in pixels (default: 200)
maxSize?: number; // Maximum size in pixels (default: Infinity)
defaultSize?: number; // Default size in pixels (default: 300)
collapsible?: boolean; // Allow collapse (default: false)
collapsed?: boolean; // Initial collapsed state (default: false)
okMaximize?: boolean; // Allow maximize (default: false)
}Drag handle between panels, used to display visual dividers between panels.
Note: The handle index is bound according to declaration order, not DOM position.
interface ResizableHandleProps {
className?: string; // CSS class name
children?: ReactNode; // Custom content, such as icons
onClick?: () => void; // Click callback
onDoubleClick?: () => void; // Double-click callback
}Access the root context value outside of ResizableContext:
import { useResizableContext, fromJson } from '@local/resizable-panels';
function GlobalControls() {
const context = useResizableContext();
// Access all groups
const groups = [...context.groups.values()];
// Save state to localStorage
const handleSave = () => {
const saved = context.getState();
localStorage.setItem('layout', JSON.stringify(saved));
};
// Load state from localStorage
const handleLoad = () => {
const json = localStorage.getItem('layout');
const state = fromJson(json);
if (state) {
context.setState(state);
}
};
return <div>Global Controls</div>;
}Subscribe to layout change events.
This is useful when you need to listen for layout changes in the sub components.
const context = useResizableContext();
// Subscribe to layout changes
const unsubscribe = context.subscribe((ctx) => {
// ...
});
// Unsubscribe when no longer needed
unsubscribe();Parameters:
callback: Function to be called when layout changes. Receives theContextValueas parameter.
Returns:
unsubscribe: Function to unsubscribe from layout changes.
Access the group context within a ResizableGroup to programmatically control panels:
import { useGroupContext } from '@local/resizable-panels';
function CustomComponent() {
const group = useGroupContext();
// Access panels and group properties
const panels = [...group.panels.values()];
}Access the panel context within a ResizablePanel:
import { usePanelContext } from '@local/resizable-panels';
function PanelContent() {
const panel = usePanelContext();
// Access panel properties
const { size, minSize, maxSize, isCollapsed, isMaximized } = panel;
return <div>Panel Size: {size}px</div>;
}Programmatically resize panels at a specific handle index. Called on GroupValue.
const group = useGroupContext();
// Expand left panel by 200px (handle index 0)
group.dragHandle(200, 0);
// Collapse right panel (handle index 1)
const panel = [...group.panels.values()][1];
group.dragHandle(-panel.size, 1);Parameters:
delta: Pixels to move (positive = panels before handle grow, negative = shrink)index: Handle index (0 = between panel 0 and 1)
Restore all panels to their state before maximization.
const group = useGroupContext();
group.restorePanels();Maximize a specific panel by collapsing all others.
const group = useGroupContext();
group.maximizePanel(targetId);Toggle maximize/restore state of a specific panel. If any panel is currently maximized, it restores all panels; otherwise, it maximizes the specified panel.
const group = useGroupContext();
const panel = usePanelContext();
// Toggle maximize/restore on button click
<button onClick={() => group.toggleMaximize(panel.id)}>
{panel.isMaximized ? 'Restore' : 'Maximize'}
</button>Get the current state of all groups as a Record object.
const context = useResizableContext();
const savedState = context.getState();
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(savedState));Apply a loaded state to all groups.
const context = useResizableContext();
context.setState(fromJson(json));Load and validate a state from a JSON string. Returns null if invalid.
import { fromJson } from '@local/resizable-panels';
const json = localStorage.getItem(LAYOUT_STORAGE_KEY);
context.setState(fromJson(json));Use ratio mode for ratio-based space distribution. Panel sizes are treated as flex-grow ratios rather than fixed pixels:
<ResizableGroup direction="col" ratio>
<ResizablePanel defaultSize={1}> {/* Takes 1/4 of space */}
Sidebar
</ResizablePanel>
<ResizablePanel defaultSize={3}> {/* Takes 3/4 of space */}
Main Content
</ResizablePanel>
</ResizableGroup>In ratio mode, resizing still works, and the sizes are distributed proportionally.
Create complex layouts by nesting groups:
<ResizableContext className="h-screen">
<ResizableGroup direction="col">
{/* Left sidebar */}
<ResizablePanel defaultSize={250} minSize={150} collapsible>
Sidebar
</ResizablePanel>
{/* Main content area */}
<ResizableGroup direction="row">
{/* Top panel */}
<ResizablePanel defaultSize={400} minSize={200}>
Top Content
</ResizablePanel>
{/* Bottom panel */}
<ResizablePanel defaultSize={300} minSize={150} collapsible>
Bottom Content
</ResizablePanel>
</ResizableGroup>
{/* Right panel */}
<ResizablePanel defaultSize={300} minSize={200}>
Right Panel
</ResizablePanel>
</ResizableGroup>
</ResizableContext>Panels with expand={true} will grow to fill available space:
<ResizableGroup direction="col">
<ResizablePanel defaultSize={200} minSize={150}>
Fixed Panel
</ResizablePanel>
<ResizablePanel expand minSize={300}>
This panel expands to fill remaining space
</ResizablePanel>
</ResizableGroup>Set collapsed={true} to make the panel initially collapsed (requires collapsible):
<ResizableGroup direction="col">
<ResizablePanel defaultSize={250} minSize={200} collapsible collapsed>
Collapsed by default sidebar
</ResizablePanel>
<ResizablePanel>
Main Content
</ResizablePanel>
</ResizableGroup>Listen to state changes when resizing ends:
<ResizableContext
onStateChanged={(context) => {
// Save state to localStorage
const state = context.getState();
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(state));
}}
>
</ResizableContext>Called when the context is mounted, useful for restoring previously saved state data:
<ResizableContext
onContextMount={(context) => {
// Load state from localStorage and apply
const json = localStorage.getItem(LAYOUT_STORAGE_KEY);
context.setState(fromJson(json));
}}
>
</ResizableContext>Use useGroupContext and utility functions to create custom panel controls:
function PanelControls() {
const group = useGroupContext();
const panels = [...group.panels.values()];
const leftPanel = panels[0];
return (
<div>
<button onClick={() => group.dragHandle(100, 0)}>
Expand Left
</button>
<button onClick={() => group.toggleMaximize(leftPanel.id)}>
Toggle Maximize
</button>
<button onClick={() => group.restorePanels()}>
Restore All
</button>
</div>
);
}MIT