Browse components
Vertical React Splitter
A stacked React splitter for charts, activity feeds, consoles, and detail 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 |
|---|---|---|
topPanel | ReactNode | Content for the top panel |
bottomPanel | ReactNode | Content for the bottom panel |
defaultSize | number | Initial height of the top panel as a percentage |
ariaLabel | string | Accessible label for the resize handle |
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.
Navigation
Workspace
Console
A flexible React splitter layout with an independent sidebar and stacked workspace panels.