Skip to content

Windmill-City/resizable-panels

Repository files navigation

EN | 中文

Resizable Panels

A headless React component library designed for building IDE-like layouts (similar to VSCode).

Features

  • 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

Installation

Build first

cd /path/to/resizable-panels
pnpm install && pnpm build

In your project's package.json add the following

{
  "dependencies": {
    "@local/resizable-panels": "link:/path/to/resizable-panels"
  }
}

Then install

cd /path/to/your-project
pnpm install

Development

This project uses pnpm workspaces. To run the example:

pnpm install
pnpm dev

If you want to strip the console logs, run:

pnpm dev:minify

Note: Do not run pnpm install in the example directory.

Due to workspace configuration, it must be run from the project root.

Quick Start

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>
  );
}

Components

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
}

ResizableGroup

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
}

ResizablePanel

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)
}

ResizableHandle

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
}

Hooks

useResizableContext

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

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 the ContextValue as parameter.

Returns:

  • unsubscribe: Function to unsubscribe from layout changes.

useGroupContext

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()];
}

usePanelContext

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>;
}

Utility Functions

dragHandle

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)

restorePanels

Restore all panels to their state before maximization.

const group = useGroupContext();
group.restorePanels();

maximizePanel

Maximize a specific panel by collapsing all others.

const group = useGroupContext();
group.maximizePanel(targetId);

toggleMaximize

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>

Layout Persistence Functions

getState

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));

setState

Apply a loaded state to all groups.

const context = useResizableContext();
context.setState(fromJson(json));

fromJson

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));

Advanced Examples

Ratio Mode

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.

Nested Layouts

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>

Expand Mode

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>

Default Collapsed

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>

State Change Callback

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>

Context Mount Callback

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>

Programmatic Panel Control

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>
  );
}

License

MIT

About

A headless React component library designed for building IDE-like layouts (similar to VSCode).

Topics

Resources

License

Stars

Watchers

Forks

Contributors