Skip to content

Splitter

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/splitter.json
See it liveA collapsible file list beside an editor, two stacked panes, and three panes with a handle between each two.→
<Splitter defaultSizes={saved ?? [25, 75]} onSizesChange={save}>
<SplitterPane min={15} max={45} collapsible>
<FileList />
</SplitterPane>
<SplitterHandle label={t('resize.files')} />
<SplitterPane min={30}>
<Editor />
</SplitterPane>
</Splitter>

Three parts, not a two-pane component. A first and second prop shape is the one every product outgrows on the day it wants a third pane, and the rewrite is every call site. Here a third pane is two more lines. Each handle moves only the boundary it sits on: the panes on either side of it trade size and the rest stay put.

Panes and handles must be direct children of the Splitter - not wrapped in a fragment or a component of the product’s own - because that is how each learns its place and how the splitter reads the panes’ limits.

Percent, not pixels. Sizes are shares of the splitter, so a window resized to half its width keeps the sidebar at the same share instead of eating the editor. A pane’s share is its flex grow over a zero basis, which is what lets the one-pixel handles sit between the panes without the shares adding up to more than the box.

The product keeps the sizes; the component never stores them. onSizesChange fires once when a change settles - a drag let go, a key pressed - not on every pointer move, so persisting there is one write per gesture. What was saved comes back as defaultSizes. Adding or removing a pane starts again from equal shares, since the old ones no longer describe the row.

The handle is the ARIA window splitter. A focusable separator with aria-orientation across the layout, aria-controls pointing at the pane before it, and that pane’s size as aria-valuenow between the bounds both neighbours allow. Arrows along the axis move it by step percent, Shift by five steps, Home and End to the bounds. Its name is a required prop: there is no English default.

Collapse is a state, not a small size. A pane marked collapsible below its minimum is either collapsed or at its minimum, never between: a drag goes to the nearer of the two, an arrow past the minimum collapses it, and an arrow back opens it at the minimum. Enter collapses the pane before the handle if it can, else the one after it - a right-hand sidebar is the second pane of its handle - and Enter again restores the size it had. A pane collapsed to nothing is inert, so Tab does not walk into controls nobody can see; collapsedSize keeps a rail of icons instead.

A pixel to see, the floor to hit. The visible line is one pixel and the hit area is target-min, the theme’s way of growing a target to 24px without growing what is drawn - the same reason ColumnResizeHandle is wider to hit than to see. Pointer events with capture, not HTML5 drag-and-drop, which a desktop shell that takes file drops never delivers to the page.

The arithmetic is exported as moveBoundary and boundsOf, plain functions a product can test or reuse without rendering anything.

Props

Read from the types in ui/splitter.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

Splitter

PropTypeDefault
orientationhorizontal | vertical'horizontal'How the panes are laid out: horizontal is side by side, with upright handles between them; vertical stacks them.
defaultSizesnumber[]One percentage per pane, summing to 100 - what was saved from onSizesChange, or the product's opening layout. Equal shares without.
onSizesChange(sizes: number[]) => voidTold the sizes when a change settles: persist them here.
stepnumber2Percent moved by an arrow key; Shift moves five of these.
childrenrequiredReactNode

And every prop of React's HTMLAttributes<HTMLDivElement> except children, passed on.

SplitterPane

PropTypeDefault
minnumberSmallest size, 0 by default.
maxnumberLargest size, 100 by default.
collapsiblebooleanWhether the pane can shrink past min to collapsedSize.
collapsedSizenumber0Its size when collapsed: 0 by default, a rail's width for a pane that keeps its icons.

And every prop of React's HTMLAttributes<HTMLDivElement> except id, passed on.

SplitterHandle

PropTypeDefault
labelrequiredstringNames what the handle resizes: "Resize the sidebar". No default - it is the product's word.

And every prop of React's HTMLAttributes<HTMLDivElement> except children, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 19 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/splitter.tsx - the file a product copies - newest first.

  1. v0.31.0
    • feat panes a person resizes, by pointer or keyboard 0e3b4db