A flexible, headless split pane library for React.
- Headless - Full control over styling with render props
- Dynamic layouts - Layout managed by state, not JSX structure
- Drag & drop - Rearrange panes by dragging
- Resizable dividers - Resize panes with constraints support
- N-way splits - Not limited to binary splits
- TypeScript - Full type safety
npm install panecake jotai bunja
# or
pnpm add panecake jotai bunjaNote:
jotaiandbunjaare peer dependencies.
import { Provider } from "jotai";
import { Root, Pane, createLayout } from "panecake";
const layout = createLayout((b) => {
const sidebar = b.leaf("sidebar");
const editor = b.leaf("editor");
const preview = b.leaf("preview");
const right = b.vertical([editor, preview], [0.6, 0.4]);
return b.horizontal([sidebar, right], [0.25, 0.75]);
});
function App() {
return (
<Provider>
<Root layout={layout}>
<Pane id="sidebar">
<Sidebar />
</Pane>
<Pane id="editor">
<Editor />
</Pane>
<Pane id="preview">
<Preview />
</Pane>
</Root>
</Provider>
);
}Use createLayout with LayoutBuilder to define your layout structure:
import { createLayout } from "panecake";
const layout = createLayout((b) => {
// Create leaf nodes (panes)
const pane1 = b.leaf("pane-1");
const pane2 = b.leaf("pane-2");
const pane3 = b.leaf("pane-3");
// Combine into branches
// horizontal: children laid out left-to-right
// vertical: children laid out top-to-bottom
const rightColumn = b.vertical([pane2, pane3], [0.5, 0.5]);
// Return the root node
return b.horizontal([pane1, rightColumn], [0.3, 0.7]);
});Layouts are tree structures with two node types:
- LeafNode - Contains a pane with content
- BranchNode - Splits children horizontally or vertically
interface LayoutState {
nodes: Record<NodeId, LayoutNode>;
rootId: NodeId | null;
}Container component that renders the layout.
<Root
layout={layout} // Layout state (controlled)
onLayoutChange={setLayout} // Layout change callback
renderDivider={CustomDivider} // Custom divider
renderDropIndicator={DropIndicator} // Custom drop indicator
className="my-pane-root"
style={{ height: "100vh" }}
>
{/* Pane components */}
</Root>Defines a pane and its content. Must be a direct child of <Root>.
// Static content
<Pane id="sidebar">
<Sidebar />
</Pane>
// With nodeId access via render prop
<Pane id="editor">
{(nodeId) => <Editor nodeId={nodeId} />}
</Pane>
// With constraints
<Pane id="panel" minWidth={200} minHeight={100}>
<Panel />
</Pane>Defines a draggable area within a pane. Without a Handle, panes cannot be dragged.
<Pane id="sidebar">
<div className="sidebar">
<Handle className="drag-handle">
<GripIcon />
</Handle>
<SidebarContent />
</div>
</Pane>Access layout state and actions from any component inside <Root>.
import { useLayout } from "panecake";
function LayoutControls() {
const {
layout, // Current layout state
setLayout, // Replace entire layout
addPane, // Add a new pane
removePane, // Remove a pane
movePane, // Move a pane
split, // Split a pane
resize, // Resize panes
} = useLayout();
const handleReset = () => {
const newLayout = createLayout((b) => {
// ... build new layout
});
setLayout(newLayout);
};
const handleAddPane = () => {
addPane("new-pane", "existing-node-id", "right");
};
const handleRemove = (nodeId: string) => {
removePane(nodeId);
};
return (
<div>
<button onClick={handleReset}>Reset</button>
<button onClick={handleAddPane}>Add Pane</button>
</div>
);
}import type { DividerRenderProps } from "panecake";
const CustomDivider = ({
direction,
onMouseDown,
onKeyDown,
ref,
}: DividerRenderProps) => (
<div
ref={ref}
className={`divider ${direction}`}
role="separator"
tabIndex={0}
onMouseDown={onMouseDown}
onKeyDown={onKeyDown}
/>
);
<Root renderDivider={CustomDivider}>
{/* ... */}
</Root>import type { DropIndicatorRenderProps } from "panecake";
const CustomDropIndicator = ({ side }: DropIndicatorRenderProps) => (
<div className={`drop-indicator drop-${side}`} />
);
<Root renderDropIndicator={CustomDropIndicator}>
{/* ... */}
</Root>Pass layout and onLayoutChange to manage state externally:
function App() {
const [layout, setLayout] = useState(initialLayout);
return (
<Root layout={layout} onLayoutChange={setLayout}>
{/* ... */}
</Root>
);
}Omit layout prop to let panecake manage state internally:
function App() {
return (
<Root>
{/* Use useLayout() hook to access/modify layout */}
</Root>
);
}For advanced use cases, access the underlying Jotai atoms directly:
import { useBunja } from "bunja/react";
import { useAtomValue, useSetAtom } from "jotai";
import { paneBunja, paneItemBunja } from "panecake";
// Root-level state
function AdvancedControls() {
const { layoutAtom, dispatchAtom, draggingNodeAtom } = useBunja(paneBunja);
const layout = useAtomValue(layoutAtom);
const dispatch = useSetAtom(dispatchAtom);
const draggingNode = useAtomValue(draggingNodeAtom);
// ...
}
// Pane-level state (inside a Pane)
function PaneContent() {
const {
nodeIdAtom,
paneIdAtom,
isDraggingAtom,
isDropTargetAtom,
dropSideAtom,
} = useBunja(paneItemBunja);
const isDragging = useAtomValue(isDraggingAtom);
const isDropTarget = useAtomValue(isDropTargetAtom);
// ...
}type PaneId = string;
type NodeId = string;
type Direction = "horizontal" | "vertical";
type DropSide = "left" | "right" | "top" | "bottom" | "center";
interface LayoutState {
nodes: Record<NodeId, LayoutNode>;
rootId: NodeId | null;
}
type LayoutNode = LeafNode | BranchNode;
interface LeafNode {
type: "leaf";
id: NodeId;
paneId: PaneId;
}
interface BranchNode {
type: "branch";
id: NodeId;
direction: Direction;
children: NodeId[];
ratios: number[];
}// Components
export { Root, Pane, Handle, Divider, NodeRenderer };
// Hooks
export { useLayout };
// Layout utilities
export { createLayout, LayoutBuilder, layoutReducer };
// State (advanced)
export { paneBunja, paneItemBunja };
export { PaneContext, PaneScope, PaneItemContext, PaneItemScope };
// Utilities
export { generateId, findParent, createLeafNode, createBranchNode };MIT