Browse components
Nested React Splitter
A flexible React splitter layout with an independent sidebar and stacked workspace panels.
Add to your project
Pick the way you like to build.
Let your AI assistant add it
Copy this prompt and paste it into the AI tool building your site.
# Integrate this UI component
Add this component to my project.
1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.
## Component source
```tsx
'use client';
import {
useId,
useRef,
useState,
type KeyboardEvent,
type PointerEvent,
type ReactNode,
} from 'react';
type Orientation = 'horizontal' | 'vertical';
type ResizablePanelsProps = {
orientation: Orientation;
firstPanel: ReactNode;
secondPanel: ReactNode;
defaultSize?: number;
minSize?: number;
ariaLabel?: string;
showHandle?: boolean;
className?: string;
};
function ResizablePanels({
orientation,
firstPanel,
secondPanel,
defaultSize = 50,
minSize = 20,
ariaLabel = 'Resize panels',
showHandle = true,
className = '',
}: ResizablePanelsProps) {
const rootRef = useRef<HTMLDivElement>(null);
const draggingRef = useRef(false);
const [isActive, setIsActive] = useState(false);
const firstPanelId = useId();
const secondPanelId = useId();
const isHorizontal = orientation === 'horizontal';
const safeMinSize = Math.min(49, Math.max(0, minSize));
const clamp = (value: number) =>
Math.min(100 - safeMinSize, Math.max(safeMinSize, value));
const [size, setSize] = useState(() => clamp(defaultSize));
const resizeFromPointer = (clientX: number, clientY: number) => {
const bounds = rootRef.current?.getBoundingClientRect();
if (!bounds) return;
const position = isHorizontal ? clientX - bounds.left : clientY - bounds.top;
const total = isHorizontal ? bounds.width : bounds.height;
setSize(clamp((position / total) * 100));
};
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
draggingRef.current = true;
setIsActive(true);
event.currentTarget.setPointerCapture(event.pointerId);
};
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
if (draggingRef.current) resizeFromPointer(event.clientX, event.clientY);
};
const handlePointerUp = (event: PointerEvent<HTMLDivElement>) => {
draggingRef.current = false;
setIsActive(false);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
const decreaseKey = isHorizontal ? 'ArrowLeft' : 'ArrowUp';
const increaseKey = isHorizontal ? 'ArrowRight' : 'ArrowDown';
const step = event.shiftKey ? 10 : 2;
if (event.key === decreaseKey || event.key === increaseKey) {
event.preventDefault();
setSize((value) =>
clamp(value + (event.key === increaseKey ? step : -step)),
);
} else if (event.key === 'Home') {
event.preventDefault();
setSize(safeMinSize);
} else if (event.key === 'End') {
event.preventDefault();
setSize(100 - safeMinSize);
}
};
return (
<div
ref={rootRef}
className={[
'relative flex overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm',
isHorizontal ? 'flex-row' : 'flex-col',
className,
]
.filter(Boolean)
.join(' ')}
>
<div
id={firstPanelId}
className='min-h-0 min-w-0 shrink-0 overflow-auto'
style={{ flexBasis: `${size}%` }}
>
{firstPanel}
</div>
<div
role='separator'
tabIndex={0}
aria-label={ariaLabel}
aria-controls={`${firstPanelId} ${secondPanelId}`}
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
aria-valuemin={safeMinSize}
aria-valuemax={100 - safeMinSize}
aria-valuenow={Math.round(size)}
onDoubleClick={() => setSize(clamp(defaultSize))}
onKeyDown={handleKeyDown}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
onLostPointerCapture={() => {
draggingRef.current = false;
setIsActive(false);
}}
className={[
'absolute z-10 touch-none bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-gray-300',
isHorizontal
? 'bottom-0 top-0 w-3 -translate-x-1/2 cursor-col-resize'
: 'left-0 right-0 h-3 -translate-y-1/2 cursor-row-resize',
].join(' ')}
style={isHorizontal ? { left: `${size}%` } : { top: `${size}%` }}
>
<span
aria-hidden='true'
className={[
isActive ? 'absolute bg-indigo-400' : 'absolute bg-gray-200',
isHorizontal
? 'bottom-0 left-1/2 top-0 w-px -translate-x-1/2'
: 'left-0 right-0 top-1/2 h-px -translate-y-1/2',
].join(' ')}
/>
{showHandle ? (
<span
aria-hidden='true'
className={[
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-gray-200 bg-white',
isHorizontal ? 'h-9 w-1.5' : 'h-1.5 w-9',
].join(' ')}
/>
) : null}
</div>
<div
id={secondPanelId}
className='min-h-0 min-w-0 shrink-0 overflow-auto'
style={{ flexBasis: `${100 - size}%` }}
>
{secondPanel}
</div>
</div>
);
}
function Placeholder({ label }: { label: string }) {
return (
<div className='flex h-full min-h-32 items-center justify-center bg-white p-6 text-sm font-medium text-gray-500'>
{label}
</div>
);
}
export type HorizontalSplitterProps = {
leftPanel?: ReactNode;
rightPanel?: ReactNode;
defaultSize?: number;
minSize?: number;
ariaLabel?: string;
showHandle?: boolean;
className?: string;
};
export function HorizontalSplitter({
leftPanel = <Placeholder label='Left panel' />,
rightPanel = <Placeholder label='Right panel' />,
defaultSize = 36,
minSize = 20,
ariaLabel = 'Resize left and right panels',
showHandle = true,
className = '',
}: HorizontalSplitterProps) {
return (
<ResizablePanels
orientation='horizontal'
firstPanel={leftPanel}
secondPanel={rightPanel}
defaultSize={defaultSize}
minSize={minSize}
ariaLabel={ariaLabel}
showHandle={showHandle}
className={className}
/>
);
}
export type VerticalSplitterProps = {
topPanel?: ReactNode;
bottomPanel?: ReactNode;
defaultSize?: number;
minSize?: number;
ariaLabel?: string;
showHandle?: boolean;
className?: string;
};
export function VerticalSplitter({
topPanel = <Placeholder label='Top panel' />,
bottomPanel = <Placeholder label='Bottom panel' />,
defaultSize = 58,
minSize = 20,
ariaLabel = 'Resize top and bottom panels',
showHandle = true,
className = '',
}: VerticalSplitterProps) {
return (
<ResizablePanels
orientation='vertical'
firstPanel={topPanel}
secondPanel={bottomPanel}
defaultSize={defaultSize}
minSize={minSize}
ariaLabel={ariaLabel}
showHandle={showHandle}
className={className}
/>
);
}
export type NestedSplitterProps = {
sidebar?: ReactNode;
topPanel?: ReactNode;
bottomPanel?: ReactNode;
defaultSidebarSize?: number;
defaultTopSize?: number;
minSize?: number;
showHandle?: boolean;
className?: string;
};
export function NestedSplitter({
sidebar = <Placeholder label='Navigation' />,
topPanel = <Placeholder label='Workspace' />,
bottomPanel = <Placeholder label='Console' />,
defaultSidebarSize = 28,
defaultTopSize = 64,
minSize = 20,
showHandle = true,
className = '',
}: NestedSplitterProps) {
return (
<HorizontalSplitter
leftPanel={sidebar}
rightPanel={
<VerticalSplitter
topPanel={topPanel}
bottomPanel={bottomPanel}
defaultSize={defaultTopSize}
minSize={minSize}
showHandle={showHandle}
className='h-full w-full rounded-none border-0 shadow-none'
/>
}
defaultSize={defaultSidebarSize}
minSize={minSize}
showHandle={showHandle}
className={className}
/>
);
}
export default HorizontalSplitter;
```
Works with your AI tools
* Compatible with most projects that use the default React/Next.js framework.
Built With
Props
| Prop | Type | Description |
|---|---|---|
sidebar | ReactNode | Content for the left sidebar |
topPanel | ReactNode | Content for the upper workspace |
bottomPanel | ReactNode | Content for the lower workspace |
defaultSidebarSize | number | Initial sidebar width as a percentage |
defaultTopSize | number | Initial upper panel height as a percentage |
minSize | number | Minimum size for either panel as a percentage |
showHandle | boolean | Shows or hides the small handle indicator on the divider |
className | string | Styles for the splitter container |
More Splitters Components
Left panel
Right panel
A clean side-by-side React splitter for inboxes, editors, and dashboard layouts.
Top panel
Bottom panel
A stacked React splitter for charts, activity feeds, consoles, and detail panels.