React Blender Panels

Blender-style panels for React. Give it a div to fill and it splits into panels you can resize by dragging an edge, and split or join by dragging a corner.

Install

npm install react-blender-panels

Here's how it works

You own the panels. Pass in an array and save the new one it hands back after every drag.

import { Panels } from 'react-blender-panels'
import type { Panel } from 'react-blender-panels'

// Each panel is a rectangle in fractions of the div (0–1).
type Panel = {
  id: string
  x: number
  y: number
  w: number
  h: number
}

const [panels, setPanels] = useState<Panel[]>([
  { id: 'viewport',   x: 0,   y: 0,   w: 0.7, h: 1   },
  { id: 'outliner',   x: 0.7, y: 0,   w: 0.3, h: 0.4 },
  { id: 'properties', x: 0.7, y: 0.4, w: 0.3, h: 0.6 },
])

<div style={{ width: '100%', height: 500 }}>
  <Panels value={panels} onChange={setPanels} />
</div>
The div filled by three panels

Your content

Pass a function as the child. It runs once per panel and returns what goes inside.

import type { Panel } from 'react-blender-panels'

const CONTENT = {
  viewport: Viewport,
  outliner: Outliner,
  properties: Properties,
}

// Panel is the minimum: add your own fields to it.
interface MyPanel extends Panel {
  kind: keyof typeof CONTENT
}

const [panels, setPanels] = useState<MyPanel[]>([
  { id: 'a', kind: 'viewport',   x: 0,   y: 0,   w: 0.7, h: 1   },
  { id: 'b', kind: 'outliner',   x: 0.7, y: 0,   w: 0.3, h: 0.4 },
  { id: 'c', kind: 'properties', x: 0.7, y: 0.4, w: 0.3, h: 0.6 },
])

<Panels value={panels} onChange={setPanels}>
  {(panel) => {
    const Content = CONTENT[panel.kind]
    return <Content />
  }}
</Panels>
Each panel's kind chooses the component rendered inside it

Gestures

Three of them, all on the panels themselves. Splits and joins preview while you drag and only commit when you let go. Escape cancels.

Resize

Drag a shared edge. Every panel touching that line moves w/ it.

Dragging a shared edge to a new position

Split

Drag a corner inward. It divides where you release. Sideways gives you a seam down the middle, up or down gives you one across.

Dragging a corner inward to divide a panel in two

Join

Drag a corner across into a neighbour. Your panel takes its space.

Dragging a corner across into a neighbour to swallow it

Styling

The panels are yours so their look is your CSS.

const style = {
  '--rbp-gap': '6px',                        // between panels, never around them
  '--rbp-edge-size': '8px',                // grab reach either side of an edge handle
  '--rbp-corner-size': '12px',               // the corner handle's square

  '--rbp-edge-color': 'rgba(0, 0, 0, 0.3)',  // the line a hovered edge handle shows
  '--rbp-corner-color': 'rgba(0, 0, 0, 0.3)',    // a hovered corner handle; defaults to the edge colour
  '--rbp-preview-border': 'rgba(0, 0, 0, 0.55)', // outline of what a release would do
  '--rbp-preview-fill': 'rgba(0, 0, 0, 0.04)',   // and its wash
  '--rbp-preview-radius': '4px',             // its corners
  '--rbp-preview-fade': '240ms',             // how long it takes to appear

  '--rbp-cursor-corner': 'crosshair',        // over a corner handle
  '--rbp-cursor-resize-x': 'col-resize',     // over a vertical line
  '--rbp-cursor-resize-y': 'row-resize',     // over a horizontal line
  '--rbp-cursor-join-e': 'e-resize',         // about to swallow the neighbour east
  '--rbp-cursor-join-w': 'w-resize',         // west
  '--rbp-cursor-join-n': 'n-resize',         // north
  '--rbp-cursor-join-s': 's-resize',         // south
  '--rbp-cursor-blocked': 'not-allowed',     // this gesture is illegal here
}

<Panels value={panels} onChange={setPanels} style={style}>
  {(panel) => {
    const Content = CONTENT[panel.kind]
    return <Content />
  }}
</Panels>

Demo

Drag an edge handle to resize. Drag a corner handle inward to split, or across into a neighbour to join.

Both demos below use this, which is all it takes to put the panels in the page's voice rather than the library's. The panels themselves are a div of ours w/ ordinary CSS, since the library never styles what goes inside one.

const style = {
  '--rbp-gap': '4px',
  '--rbp-edge-color': 'rgba(0, 0, 0, 0.25)',
  '--rbp-preview-border': '#1d1d1f',
  '--rbp-preview-radius': '4px',
}

<Panels value={panels} onChange={setPanels} style={style}>
  {(panel) => <div className="demo-panel">{panel.id}</div>}
</Panels>
.demo-panel {
  display: grid;
  place-items: center;
  border: 1px solid #1d1d1f;
  border-radius: 4px;
  background: #fff;
  overflow: hidden;
}
viewport
outliner
properties

Demo without handles

Same gestures, nothing to see. Only the colours are off, so every drag still works.

const style = {
  '--rbp-gap': '4px',
  '--rbp-edge-color': 'transparent',
  '--rbp-corner-color': 'transparent',
}
viewport
outliner
properties