Appearance
useSplitter
A hook for resizable panel layouts that supports dragging and keyboard interactions.
Demo
Usage
tsx
import { useSplitter } from '@reause/core'
const splitter = useSplitter({
panels: [
{ defaultSize: 30, min: 10, collapsible: true },
{ defaultSize: 70 },
],
})
// <div ref={splitter.ref} style={{ display: 'flex', height: 300 }}>
// <div style={{ flexGrow: splitter.sizes[0] }} />
// <div {...splitter.getHandleProps({ index: 0 })} />
// <div style={{ flexGrow: splitter.sizes[1] }} />
// </div>Separator Attributes and Interactions (getHandleProps)
getHandleProps returns all accessible attributes and event handlers required for the separator handle:
- Accessibility Attributes:
role="separator",aria-orientation,aria-valuenow/aria-valuemin/aria-valuemax(calculated based on the size and boundaries of the left/top panel),tabIndex. - State and Style Markers:
data-active,data-orientation. - Keyboard and Double-Click Interactions:
Arrow keys: Adjust adjacent panels incrementally bystep(usesshiftStepwhen holdingShift; direction is reversed indir: 'rtl'mode).Home/End: Instantly snap the left/top panel to its minimum/maximum limit.Enter: Toggle the collapsed state of the smaller adjacent collapsible panel.Double-click: Trigger a reset (enabled by default whenresetOnDoubleClickistrue).
Unit Control and Pixel Mode (pixelMode)
Panel sizes support CSS unit declarations:
- Flexible Mode: Pure numbers without units or
%strings, distributing remaining space based on weight. - Fixed Mode: Strings containing
pxorrem.
If any fixed unit appears in panel sizes, min, max, collapseThreshold, step, shiftStep, or controlled sizes, the hook automatically enables pixelMode. In this mode, all sizes are parsed and converted to pixels, and pure numbers are interpreted as container percentages rather than relative weights. Developers can render synchronously using the returned pixelMode.
tsx
// Pixel mode example: Sidebar fixed at 240px, content area responsive (sidebar remains 240px wide on container resize)
const splitter = useSplitter({
panels: [{ defaultSize: '240px', min: '120px' }, { defaultSize: 100 }],
})
// Vertical layout example: Automatically switches axes, cursors, and arrow key responses
const vertical = useSplitter({
panels: [{ defaultSize: 50 }, { defaultSize: 50 }],
orientation: 'vertical',
})Size Update Mechanism
- Unit Preservation: Panels retain their declared unit type after resizing (for example,
'240px'remains'240px'after dragging, while its adjacent flexible panel automatically adapts to percentage). - Collapse Control: Calling
collapse(panelIndex)orexpand(panelIndex)can absorb panel size into adjacent panels or restore the snapshot size saved prior to collapsing. - Space Reset: Calling
reset(handleIndex)restores the default ratios of adjacent panels while preserving their combined size.
Redistribution Mode (redistribute)
By default, dragging only adjusts the two panels immediately adjacent to the separator. To push beyond this limitation and affect outer panels, configure redistribute:
'nearest': Prioritizes taking space from the nearest panel in the drag direction.'equal': Distributes the size delta evenly across all panels in the drag direction.Custom Function: Pass a custom function({ sizes, panels, handleIndex, delta }) => resolvedSizesfor precise control.
tsx
const splitter = useSplitter({
panels: [
{ defaultSize: 26 },
{ defaultSize: 20, min: 20 },
{ defaultSize: 54 },
],
redistribute: 'nearest',
})Controlled Mode and Event Listeners
Supports passing sizes for controlled management, with event listeners for monitoring state changes:
onSizeChange: Triggered when sizes change (in controlled mode, this only notifies and does not automatically update internal state).onResizeStart/onResizeEnd: Callbacks for the start and end of pointer dragging.onCollapseChange: Triggered when a panel toggles between collapsed and expanded states.enabled: false: Disables pointer and keyboard interactions in one toggle.
Type Declarations
Toggle
ts
export type SplitterPaneSize = number | `${number}%` | `${number}px` | `${number}rem`
export type SplitterStep = number | `${number}%` | `${number}px` | `${number}rem`
export interface UseSplitterPanel {
defaultSize: SplitterPaneSize;
min?: SplitterPaneSize;
max?: SplitterPaneSize;
collapsible?: boolean;
collapseThreshold?: SplitterPaneSize;
}
export type UseSplitterRedistributeFn = (input: UseSplitterRedistributeInput) => number[]
export interface UseSplitterRedistributeInput {
sizes: number[];
panels: UseSplitterResolvedPanel[];
handleIndex: number;
delta: number;
}
export interface UseSplitterResolvedPanel {
defaultSize: number;
min?: number;
max?: number;
collapsible?: boolean;
collapseThreshold?: number;
}
export interface UseSplitterOptions {
panels: UseSplitterPanel[];
orientation?: 'horizontal' | 'vertical';
sizes?: SplitterPaneSize[];
onSizeChange?: (sizes: SplitterPaneSize[]) => void;
onResizeStart?: (handleIndex: number) => void;
onResizeEnd?: (handleIndex: number, sizes: SplitterPaneSize[]) => void;
onCollapseChange?: (panelIndex: number, collapsed: boolean) => void;
redistribute?: SplitterRedistribute;
step?: SplitterStep;
shiftStep?: SplitterStep;
dir?: 'ltr' | 'rtl';
resetOnDoubleClick?: boolean;
enabled?: boolean;
}
export interface UseSplitterReturnValue<T extends HTMLElement = any> {
ref: React.RefCallback<T | null>;
sizes: SplitterPaneSize[];
pixelMode: boolean;
collapsed: boolean[];
activeHandle: number;
getHandleProps: (input: {
index: number;
}) => {
'ref': React.RefCallback<HTMLElement>;
'role': 'separator';
'aria-orientation': 'horizontal' | 'vertical';
'aria-valuenow': number;
'aria-valuemin': number;
'aria-valuemax': number;
'tabIndex': number;
'onKeyDown': React.KeyboardEventHandler;
'onDoubleClick': React.MouseEventHandler;
'data-active': boolean | undefined;
'data-orientation': 'horizontal' | 'vertical';
};
setSizes: (sizes: SplitterPaneSize[]) => void;
collapse: (panelIndex: number) => void;
expand: (panelIndex: number) => void;
toggleCollapse: (panelIndex: number) => void;
reset: (handleIndex: number) => void;
}
export type SplitterRedistribute = 'nearest' | 'equal' | UseSplitterRedistributeFn
export function useSplitter<T extends HTMLElement = any>(options: UseSplitterOptions): UseSplitterReturnValue<T>
export function applyConstraints(sizes: number[], panels: UseSplitterResolvedPanel[], handleIndex: number, delta: number, redistribute?: SplitterRedistribute): number[]