Skip to contentAsheeUIAsheeUI

ResizableScreen

Two panels and a handle between them, so the reader decides how the width is divided.

A resizable screen is two panels with a handle that moves the boundary between them. The first panel takes the size you give it, the second takes whatever is left, and the reader adjusts the division by dragging the handle or by focusing it and pressing an arrow key. It is the layout of an editor with a file tree, a mail client with a list beside a message, and a data tool with a query beside its results.

It is the interactive counterpart of Split: a split states a ratio and keeps it, while a resizable screen hands the ratio to the reader. The component is web-only, because a draggable divider is a desktop idiom.

The handle is a real separator rather than a decorated border: dragging it captures the pointer, the arrow keys move the boundary by step, and Home and End jump to the clamps. hideHandle renders the same split without it, for the case where the division is fixed today but the layout is not.

Usage

import { ResizableScreen } from "@asheeui/web";

<ResizableScreen defaultSize={35} minSize={20} maxSize={60}>
  <nav aria-label="Documents">
    <ul>
      <li>INV-1041</li>
    </ul>
  </nav>
  <article>
    <p>The invoice.</p>
  </article>
</ResizableScreen>

children is a pair of exactly two nodes, and the component reads them as a tuple: the first is the panel the percentage applies to, the second takes the remainder, and the handle is rendered between them. One child is a type error rather than a silent layout, because a split with one panel has nothing to divide, that case is Split or a plain Stack.

A size that is controlled or not

defaultSize seeds an uncontrolled split. The component owns the number, dragging changes it, and onSizeChange is told what it became, so a page that only wants to react can listen without taking ownership.

When size is given the component is controlled: it draws that percentage and reports every request through onSizeChange, and the boundary only moves when the page writes the number back. left is an alias for defaultSize, kept for callers that named the first panel's position rather than its size.

Sizes are percentages, so a split keeps its proportions when the window changes. A controlled split therefore leaves the page the owner of a number it has to keep in state beside whatever else that panel's width decides, which is the reason to control it in the first place.

Clamps, steps and orientation

minSize and maxSize clamp the first panel as a percentage, 20 and 80 unless the instance or components.resizableScreen say otherwise. Every path into the number runs through the clamp: the drag, the arrow keys, Home and End, and the value onSizeChange reports. step is how far one arrow press moves the boundary, 2 by default.

orientation states which way the split divides. horizontal puts the panels side by side and resizes with ← and →; vertical stacks them and resizes with ↑ and ↓.

minContainerWidth and minContainerHeight are pixels rather than percentages, and they exist because a split inside a parent with no definite size would otherwise collapse: the container claims at least 200 pixels of each unless the instance or components.resizableScreen say otherwise, and a definite height from the layout around it, h-64, a flex row, a grid row, is what the percentage is measured against. The two are configuration as well as instance props, so a product whose panels are never smaller than a particular size states the floor once.

Props

defaultSize, minSize, maxSize, step, orientation, minContainerWidth and minContainerHeight are registered by the component as 50, 20, 80, 2, horizontal, 200 and 200, so each resolves from the instance, then components.resizableScreen, then that value. hideHandle is not registered: its fallback is false, so the handle is drawn unless the instance or the application hides it. left, size, onSizeChange and handleClassName are statements about one split rather than about the product, so they are instance props.

Accessibility

  • The handle is a focusable role="separator" with tabindex="0", the pattern ARIA defines for a resizable split. It carries a name (Resize panel split), aria-valuenow with the first panel's percentage, aria-valuemin and aria-valuemax from the clamps, and aria-orientation describing the separator's own axis: a side-by-side split is divided by a vertical separator, so it reports vertical, and a stacked split reports horizontal. That is the line the separator draws, the opposite of the axis the panels are laid out on, and the arrows move the separator across it, which is why a vertical one answers to ← and →.
  • The keyboard contract is complete: the arrows move the boundary by step in the direction orientation implies, and Home and End jump to the minimum and the maximum. The handle can therefore be used without a pointer, which is what a resizable split usually fails at.
  • The drag captures the pointer, so a fast drag that leaves the handle keeps resizing, and the handle is touch-none, so a scroll gesture is not stolen while the drag is in flight.
  • Each panel scrolls on its own, so a long list in the first panel does not push the second out of view, and the panels keep the order they are written in for a reader moving through the document.

A resizable screen is the interactive end of the layout family; the static arrangements it sits beside are described once in Layout and spacing.

Native

The compatibility matrix classifies this component as web-only, and the reason is the interaction rather than the arrangement: a draggable divider is a desktop idiom, and a native split view, where a platform offers one, is the platform's to draw, with its own gestures and its own presentation. @asheeui/native exports nothing for it.

What to reach for on a device: Split for the static two-pane layout, which stacks the panes on a narrow screen, and the platform's own split view when the user really must move the boundary. See React Native.