LayoutReact
Panels side by side, or stacked, with a Separator between each pair that resizes the two around it.
Rendered by React 19.3.1
Loading React…Installation
npm i @hamur/reactNot on npm yet. The package goes out under the chosen name.
Anatomy
Import the component and assemble its parts:
<Splitter.Root>
<Splitter.Panel />
<Splitter.Separator />
</Splitter.Root>Examples
Vertical
orientation vertical: Editor above Terminal, and the line moves with the up and down arrows.
Three
Three panels with minSize and maxSize, named by the headings inside them, so each line reports its own panel's range.
Collapsible
A collapsible sidebar that collapses to a 14% rail of icons with a flex-grow transition, from Enter, a double-click or a Sidebar button that calls actionsRef.
Nested
A vertical Root inside a panel of a horizontal one.
Pixel
A sidebar at 140 px between 100 and 200 px with resizeBehavior set to keep its width; the box resizes from its corner.
Controlled
value in the example's state: onValueChange writes it and its reason, and three buttons outside set it.
Persist
onValueCommitted writes the layout to a cookie that defaultValue reads on the next load; an app reads it on the server.
API reference
22 props across 3 parts, in React syntax. className, style, render are listed once, under Shared props.
PanelSplitter.Panel
A resizable pane. Its size is its entry in the Root's value. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
valueThe panel's key in the Root's value. | string | — |
defaultSizeThe panel's size when no value or defaultValue entry gives one: a number in percent, or | SplitterSize | — |
minSizeThe smallest size the panel can have, in percent or with a unit. | SplitterSize | 0 |
maxSizeThe largest size the panel can have, in percent or with a unit. | SplitterSize | 100 |
collapsibleWhether the panel collapses to its collapsedSize below its threshold. | boolean | false |
collapsedSizeThe size of the panel when collapsed, in percent or with a unit. | SplitterSize | 0 |
collapseThresholdHow far above collapsedSize a shrinking panel collapses, in percent or with a unit. | SplitterSize | — |
resizeBehaviorHow the panel's size follows a change of the container's size: 'preserve-pixel-size' | SplitterResizeBehavior | 'preserve-relative-size' |
disabledWhether the panel neither gives nor takes space when a Separator moves. | boolean | false |
| Data attribute | Description |
|---|---|
data-orientation | The layout axis. |
data-collapsed | Present when the panel is collapsed to its collapsedSize. |
data-disabled | Present when the panel is disabled. |
RootSplitter.Root
Groups the panels and the separators between them, and lays them out on its axis. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
valueEach panel's size in percent of the space the panels share, keyed by the panel's value. | SplitterValue | — |
defaultValueThe panels' sizes when the component is initially rendered, in percent, keyed by the panel's | SplitterValue | — |
onValueChangeEvent handler called on every change of the layout, including each move of a drag. | ((value: SplitterValue, eventDetails: SplitterRootChangeEventDetails) => void) | — |
onValueCommittedEvent handler called when a change is committed: pointer release, each key press, | ((value: SplitterValue, eventDetails: SplitterRootChangeEventDetails) => void) | — |
orientationThe layout axis: 'horizontal' puts the panels side by side. | SplitterOrientation | 'horizontal' |
disabledWhether no Separator moves. | boolean | false |
stepThe percent an arrow key moves a Separator by. | number | 1 |
largeStepThe percent Shift+Arrow, Page Up and Page Down move a Separator by. | number | 10 |
localeThe locale of the default aria-valuetext. | Intl.LocalesArgument | — |
actionsRefA ref to imperative actions: resize(panel, size), collapse(panel), expand(panel) and | React.RefObject<SplitterRootActions | null> | — |
| Data attribute | Description |
|---|---|
data-orientation | The layout axis. |
data-dragging | Present while a Separator is being dragged. |
data-disabled | Present when the splitter is disabled. |
SeparatorSplitter.Separator
The line between two panels that resizes them, by pointer and by keyboard. It is static on the server and until the panels around it are known. Renders a <div>.
| Prop | Type | Default |
|---|---|---|
primaryPanelThe panel the separator reports and names itself after: the one before it or after it. | SplitterPrimaryPanel | 'before' |
disabledWhether the separator is static: not focusable, no values, does not move. | boolean | false |
getAriaValueTextAccepts a function which returns a string value that provides a user-friendly name for the | ((formattedValue: string, value: number) => string) | — |
| Data attribute | Description |
|---|---|
data-orientation | The line the separator draws: vertical in a horizontal splitter. |
data-dragging | Present while the separator is being dragged. |
data-disabled | Present when the separator is disabled. |
data-at-min | Present when the primary panel is at the smallest size it can reach. |
data-at-max | Present when the primary panel is at the largest size it can reach. |
Shared props
Every part that renders an element takes these.
| Prop | Type |
|---|---|
classNameCSS class applied to the element, or a function that
returns a class based on the component's state. | string | ((state: State) => string | undefined) |
styleStyle applied to the element, or a function that
returns a style object based on the component's state. | React.CSSProperties | ((state: State) => React.CSSProperties | undefined) |
renderAllows you to replace the component's HTML element
with a different tag, or compose it with another component. | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State> |
Keyboard and accessibility
Keyboard
| Keys | Does | Part |
|---|---|---|
| ArrowLeftArrowRight | Move the line left or right by step, 1% by default | Separator |
| ShiftArrowLeftShiftArrowRight | Move the line by largeStep, 10% by default | Separator |
| ArrowUpArrowDown | Move the line up or down when orientation is vertical; in a horizontal Root, nothing | Separator |
| ArrowLeftArrowRight | Still move the line left and right in a right-to-left layout | Separator |
| PageUpPageDown | Grow or shrink the primary panel by largeStep | Separator |
| HomeEnd | Shrink or grow the primary panel as far as every panel's limits allow | Separator |
| Enter | Collapses a collapsible primary panel, and restores its size on a second press | Separator |
| Tab | Skips the content of a panel collapsed to 0 | Panel |
Accessibility
| Part | Sets | So that |
|---|---|---|
| Separator | role="separator" with tabindex="0", aria-valuenow, aria-valuemin and aria-valuemax of its primary panel, in the range every panel's limits leave it | a reader hears the panel's size and how far it can go |
| Separator | aria-orientation of the line: vertical in a horizontal Root, horizontal in a vertical one; data-orientation the same | the arrows a reader is told to use are the ones that move it |
| Separator | aria-valuetext "35%" (from getAriaValueText when given, in the Root's locale), aria-controls the primary panel's id | the value is said as a percentage and the separator points at what it resizes |
| Separator | its own aria-label or aria-labelledby, else the primary panel's; a development warning when neither has one | each line has a name |
| Separator | static (the role and aria-orientation only, no Tab stop) on the server and until the panels around it are known, and when disabled | a focusable separator never lacks its value |
| Panel | inert while collapsed to 0; a rail with a collapsedSize above 0 stays usable | Tab, find and a screen reader skip what is not shown |
| Panel | no role and no aria-disabled; the app makes a pane a landmark with render (a nav, a main) | the layout adds no landmark of its own |
| Root | no role and no ARIA; data-dragging while a line is dragged, with user-select: none inline | the layout is not announced, and a drag does not select text |
Focus order
- In document order: a panel's content, then the separator after it, then the next panel's content. Nothing takes focus on mount.
- A press on a separator focuses it without a focus ring, and focus stays there after the drag, so the arrows can refine what the pointer did.
- The content of a panel collapsed to 0 is skipped until the panel is restored.
- In nested splitters each separator moves only its own Root's panels.