Splitter
Overview
A composable layout component that divides an area into resizable panels.
Features
- Three-part composition —
SSplitterGroup(root),SSplitterPanel(content region),SSplitterResizeHandle(drag handle between panels). - Horizontal and vertical directions —
direction="horizontal" | "vertical"flips the layout axis; cursor and ARIA orientation adapt automatically. - Panel size constraints — each panel supports
defaultSize,minSize,maxSize, andcollapsedSizefor fine-grained control over resize bounds. - Collapsible panels —
collapsiblepanels can collapse tocollapsedSizevia the Enter key or the imperative API, withcollapse/expand/resizeevents. orderprop — panels declareorderto control the logical sequence fordefaultLayoutmapping and pair resizing, independent of DOM order.defaultLayout— provide an explicit initial layout array (maps to sorted panels byorder).- Pointer dragging — pointer-event-based dragging with
pointerIdtracking,pointermove/pointerup/pointercancellisteners on the owner document, and automatic cleanup on unmount. - Keyboard resizing — Arrow keys resize by
keyboardResizeBy(default 10%); Home / End jump to min / max; Enter toggles collapse. - RTL support —
dir="rtl"inverts both pointer delta and arrow-key direction for horizontal splitters; the rootdirattribute is set fromuseDirection(falls back toConfigProvider). - Imperative panel API —
SSplitterPanelexposescollapse(),expand(),resize(size),getSize(),isCollapsed, andisExpandedviadefineExposefor programmatic control. - Full ARIA separator semantics — the handle uses
role="separator"witharia-controls,aria-orientation,aria-valuenow,aria-valuemin,aria-valuemax, andaria-disabled. - Size scaling —
size(xs…2xl) scales the resize handle’s visual grip width viasplitterVariants. - Custom handle content — pass a default slot to
SSplitterResizeHandlefor a fully custom grip, or usewithHandlefor the built-in dotted grip. - State reflection — panels expose
data-state="collapsed|expanded"(when collapsible),data-panel-size,data-panel-collapsible; handles exposedata-state="drag|hover|inactive",data-disabled,data-orientation. - Aria composition —
SplitterGroup,SplitterPanel,SplitterResizeHandleare exported from@vean/aria/splitterfor custom styled builds.
Usage
Demo
Basic
Collapsible
Custom Handle
API
Aria Layer
3SplitterGroup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
ui | Partial<SplitterUi> | — | Per-slot class overrides for the component. |
size | ThemeSize | — | Visual size of the component. |
direction | DataOrientation | — | The orientation of the splitter group.
Either `horizontal` or `vertical`. Defaults to `horizontal`. |
dir | Direction | — | The reading direction of the splitter group. |
defaultLayout | number[] | — | The initial layout percentages for the panels. |
keyboardResizeBy | number | — | The percentage step size used for keyboard resizing. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
layout | number[] | Event handler called when the group layout changes. |
SplitterPanel
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
defaultSize | number | — | The initial size of the panel as a percentage of the group. |
collapsible | boolean | — | Whether the panel can collapse. |
collapsedSize | number | — | The size of the panel when collapsed. |
maxSize | number | — | The maximum size of the panel. |
minSize | number | — | The minimum size of the panel when expanded. |
order | number | — | The order of the panel within the group. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
collapse | — | Event handler called when the panel collapses. |
expand | — | Event handler called when the panel expands. |
resize | number, prevSize: number | undefined | Event handler called when the panel size changes. |
SplitterResizeHandle
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
withHandle | boolean | — | Whether with handle. |
disabled | boolean | — | Whether the resize handle is disabled. |
tabindex | number | — | The tabindex of the resize handle. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
dragging | boolean | Event handler called when dragging state changes. |
Notes
Architecture and benchmark comparison
| Concern | VeanUI | react-resizable-panels | Ant Design ResizeBox / Splitter |
|---|---|---|---|
| Aria / styled separation | ✅ @vean/aria/splitter ships logic; @vean/ui ships scv() recipe | ❌ React-only, no headless layer | ❌ single styled package |
| Panel size constraints | defaultSize / minSize / maxSize / collapsedSize | defaultSize / minSize / maxSize / collapsedSize | min / max |
| Collapsible panels | collapsible + Enter key + imperative API | collapsible + onCollapse / onExpand | — |
Panel order | order prop controls logical sequence for layout mapping | order prop | — |
| Keyboard resizing | Arrow keys + Home / End + Enter (collapse toggle) | Arrow keys + Home / End + Enter | — |
| RTL support | dir prop + useDirection fallback; inverts pointer delta and arrow keys | direction prop with RTL awareness | — |
| Imperative panel API | defineExpose (collapse / expand / resize / isCollapsed) | ImperativePanelHandle | — |
| ARIA separator semantics | role="separator" + aria-controls / aria-valuenow / aria-valuemin / aria-valuemax | same | partial |
| Layout persistence | layout event (consumer persists) | autoSaveId built-in persistence | — |
| Dynamic panel mount/unmount | registerPanel / unregisterPanel + refreshLayout | supported | — |
| Dragging event | dragging event on handle | onDragging | — |
Runtime considerations
- Layout is percentage-based — all sizes are percentages of the group’s main-axis size.
getGroupSize()readsgetBoundingClientRect()on the group element to convert pixel delta to percentage delta during drag. orderaffects layout mapping, not DOM position — panels are sorted byorderfordefaultLayoutassignment and pair resizing, but their visual position is determined by template order. Useorderwhen you need the layout array to map to panels in a specific logical sequence.defaultLayoutmaps to sorted panels — when provided,defaultLayout[i]is assigned to the panel at sorted positioni(byorder, then registration index). WithoutdefaultLayout, each panel uses its owndefaultSize.- Collapsible threshold — when dragging a collapsible panel below the midpoint between
collapsedSizeandminSize, the panel snaps tocollapsedSize; above the midpoint, it snaps tominSize. distributeDeltaguard — the delta distribution loop is capped at 20 iterations to prevent infinite loops when panels are tightly constrained. A fallback pass assigns any remainder to the first eligible panel.- Pointer listeners are on the owner document —
pointermove/pointerup/pointercancelare attached tohandleElement.ownerDocument(not the handle) so dragging continues even when the cursor leaves the handle. Listeners are cleaned up onpointerup/pointercanceland on component unmount. useDirectionfallback — ifdirprop is unset, the direction falls back toConfigProvider’sdir, then to'ltr'. The resolveddiris set on the group root element.
FAQ
How do I make a panel collapsible?
Set collapsible on SSplitterPanel and provide a collapsedSize (default 0). Users can press Enter on the adjacent resize handle to toggle collapse. You can also call panelRef.value.collapse() / expand() programmatically.
How does order work?
order controls the logical sequence of panels for layout calculation. defaultLayout array indices map to panels sorted by order (then by registration index for ties). This is useful when the DOM order doesn’t match the desired logical order — for example, when a sidebar panel is visually first but should be logically second.
How do I persist the layout?
Listen to the layout event on SSplitterGroup and save the array (e.g., to localStorage). On next mount, pass the saved array as defaultLayout. Unlike react-resizable-panels’ autoSaveId, VeanUI leaves persistence to the consumer for flexibility.
Can I disable a resize handle?
Yes — set disabled on SSplitterResizeHandle. The handle gets aria-disabled="true", data-disabled, tabindex is removed, and pointer / keyboard interactions are blocked.
How does RTL work?
Set dir="rtl" on SSplitterGroup (or rely on ConfigProvider). In RTL mode, dragging right shrinks the first panel (opposite of LTR), and ArrowLeft / ArrowRight are inverted. The root element gets dir="rtl".
How do I customize the resize handle?
Pass a default slot to SSplitterResizeHandle for fully custom content. Alternatively, set withHandle to render the built-in dotted grip. Without either, the handle is a plain line.
Can I dynamically add or remove panels?
Yes — panels register on mount and unregister on unmount. The layout is recalculated automatically via refreshLayout. When a panel unmounts, its size is redistributed to the remaining panels.