JGuang

Resizable

Resizable · Astryx

Preview
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0

Resizable

Hook-based resizable panel system. useResizable() manages size state and ResizeHandle provides the interactive pill-grip separator. Pass resize props to existing layout components via their resizable prop.

Installation

npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0

Requires React 19+, base CSS and a Theme provider. See Setup.

Resizable

Hook-based resizable panel system. useResizable() manages size state and ResizeHandle provides the interactive pill-grip separator. Pass resize props to existing layout components via their resizable prop.

Usage guidance

  • Use percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) when a percentage needs exactly one pixel floor or ceiling. Reuse Table’s pixel() helper; numbers, exact Npx, and pixel(value) remain pixels. State, persistence, callbacks, paint, and ARIA remain resolved pixels.
  • Use useResizable() with existing Astryx layout components. Pass the returned props to the resizable prop on LayoutPanel or SideNav.
  • Provide an accessible label on each ResizeHandle when multiple handles exist (e.g. "Resize sidebar", "Resize terminal").
  • Avoid: Wrap panels in extra container components for resize. The hook-first architecture avoids extra DOM; use it directly on existing components.

Theming API

{
  "targets": [
    {
      "className": "astryx-resize-handle",
      "visualProps": [
        "direction"
      ]
    },
    {
      "className": "astryx-resize-handle-pill"
    }
  ]
}

useResizable

Hook that manages resize state for one or more panel regions. Returns size, isCollapsed, collapse/expand/resize methods, and props to pass to handles.

PropTypeDefaultDescription
defaultSizeSizeValue | ResizableSize250Initial size. Numbers, exact Npx, and pixel(value) are pixels; exact N% has no additional pixel bound; percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds exactly one and resolves once.
minSizeResizableSize50Live minimum. Numbers, exact Npx, and pixel(value) remain pixels; exact N% has no additional pixel bound; percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds exactly one.
maxSizeResizableSizeInfinityLive maximum. Numbers, exact Npx, and pixel(value) remain pixels; exact N% has no additional pixel bound; percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds exactly one.
collapsiblebooleanfalseWhether the region can collapse to size 0.
collapsedSizenumber40Pixel threshold that triggers collapse during drag.
snapsnumber[]Pixel values to snap to during resize.
shrinkOrdernumberCascade priority: lower number shrinks first.
autoSaveIdstringKey for persisting sizes and collapse state to localStorage.

Structured fixed pixels

const region = useResizable({
  defaultSize: pixel(333),
  containerRef,
});

One-time structured default

import {useRef} from 'react';
import {ResizeHandle, useResizable} from '@astryxdesign/core/Resizable';
import {percent, pixel} from '@astryxdesign/core/Resizable/utils';

const containerRef = useRef<HTMLDivElement>(null);

const region = useResizable({
  defaultSize: percent(40, {min: pixel(333)}),
  containerRef,
});

<ResizeHandle direction="horizontal" resizable={region.props} />;

Live percentage floor

const region = useResizable({
  defaultSize: 0,
  minSize: percent(40, {min: pixel(333)}),
  containerRef,
});

Live percentage ceiling

const region = useResizable({
  defaultSize: 500,
  maxSize: percent(10, {max: pixel(400)}),
  containerRef,
});

Resize Handle

Draggable separator between panels. Pill-grip design: invisible at rest, visible on hover (0.6 opacity), fully opaque during drag (1.0). Keyboard-accessible.

PropTypeDefaultDescription
direction'horizontal' | 'vertical''horizontal'Layout direction: determines cursor and indicator orientation.
isReversedbooleanfalseReverse drag direction. Use when the handle controls a panel on the end/right/bottom side.
isDisabledbooleanfalseWhether the handle is interactive.
hasDividerbooleanfalseShow a full-length 1px divider line through the handle. Use when adjacent panels share the same background.
isAlwaysVisiblebooleantrueShow the pill grip at rest instead of only on hover. Use when discoverability is important.
pillPlacement'start' | 'end' | 'center' | 'auto''auto'Which side of the divider the pill sits on. auto = content side (derived from isReversed), flips when collapsed. start = left/top, end = right/bottom, center = centered on divider.
labelstring'Resize handle'Accessible label for the separator.
resizable *ResizablePropsResize props from useResizable: connects handle to panel. Carries the region's axis ('horizontal' | 'vertical'), which must match this handle's direction.
childrenReactNodeCustom handle content. Overrides the default pill + divider.
xstyleStyleXStylesStyleX styles for layout customization (margins, positioning, sizing). Must be a stylex.create() value, not an inline style object like style={{}}.

Anatomy

  • Handle: Focusable separator that owns pointer and keyboard resize interaction.
  • Grip pill: Default visible grip indicator; custom handle content can replace it.
  • Grab zone: Invisible enlarged pointer region aligned with the grip or divider.

useResizable

Hook for adding drag-to-resize behavior to layout regions. Supports single-region and multi-region configurations with snap points, collapsible panels, localStorage persistence, and cascade resize ordering.

Parameters

ParameterTypeDescription
defaultSizeSizeValue | ResizableSizeInitial size. Numbers, exact "Npx", and pixel(value) are pixels. Exact "N%" has no additional pixel bound. percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds one pixel floor or ceiling. A percentage resolves ONCE into pixels — against containerRef when supplied, against the viewport otherwise — and does not track its basis afterwards. The released broad number | string type remains compatible; runtime validation is authoritative.
minSizeResizableSizeMinimum size. Numbers, exact "Npx", and pixel(value) remain pixels; exact "N%" has no additional pixel bound; percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds exactly one. Percentage minimums re-resolve when their basis changes and clamp the current pixel selection.
maxSizeResizableSizeMaximum size. Numbers, exact "Npx", and pixel(value) remain pixels; exact "N%" has no additional pixel bound; percent(value, {min: pixel(value)}) or percent(value, {max: pixel(value)}) adds exactly one. Percentage maximums re-resolve when their basis changes and clamp the current pixel selection.
containerRefRefObject<HTMLElement | null>The element a percentage is a share of. Caller-owned: the hook never infers one. Omitted, percentages use the viewport, which is the released behaviour. The ref may point at a different element over time — the basis follows it. Until that element is actually laid out (not yet mounted, display:none, detached) percentages use a temporary 1200px basis rather than its zero measurement, and nothing is persisted from it.
direction'horizontal' | 'vertical'Which axis this region resizes along. Selects the container's inline or block content-box size as the percentage basis, and must match the direction given to ResizeHandle.
collapsiblebooleanWhether dragging below the collapsed threshold collapses the region to zero.
snapsnumber[]Pixel values to snap to during drag.
autoSaveIdstringKey for localStorage persistence of size and collapse state across sessions.
defaultIsCollapsedbooleanInitial collapse state (uncontrolled). A persisted entry wins over it.
isCollapsedbooleanControlled collapse state. collapse(), expand() and a drag past the threshold then report through onCollapseChange instead of changing state internally.
onCollapseChange(isCollapsed: boolean) => voidCalled once per collapse state change, via drag or programmatically.

Returns

[
  {
    "name": "size",
    "type": "number",
    "description": "Current size in pixels."
  },
  {
    "name": "isCollapsed",
    "type": "boolean",
    "description": "Whether the region is currently collapsed."
  },
  {
    "name": "collapse",
    "type": "() => void",
    "description": "Programmatically collapse the region."
  },
  {
    "name": "expand",
    "type": "() => void",
    "description": "Expand from collapsed state."
  },
  {
    "name": "resize",
    "type": "(size: number) => void",
    "description": "Resize to a specific pixel value."
  },
  {
    "name": "props",
    "type": "ResizableProps",
    "description": "Props to spread on the resizable component or pass to ResizeHandle."
  }
]

Usage guidance

  • Use percent(40, {min: pixel(333)}) for a 40% size with a 333px floor, or percent(10, {max: pixel(400)}) for a 10% size with a 400px ceiling. The options argument is required and carries a floor XOR a ceiling.
  • A structured default is an initial choice only; a structured minSize or maxSize remains live. State, persistence, callbacks, resize(), paint, and ARIA all use resolved pixel numbers.
  • Import percent and Table’s same pixel binding from @astryxdesign/core/Resizable/utils when constructing configuration in a Server Component; the root package exposes one pixel symbol and one percent symbol without collision.
  • Use with Layout or AppShell sidebar for resizable navigation panels.
  • Set autoSaveId to persist user-chosen sizes across page reloads.
  • Avoid: Set minSize too small; content becomes unreadable. Prefer collapsible for panels that can hide entirely.

Examples

  • Resizable: Horizontal resizable split with a draggable handle between two panels.
  • Resizable — Collapsible with snap points: A collapsible sidebar with snap points, driven by useResizable. Dragging snaps to preset widths, dragging past the minimum collapses the panel, and the expand method restores it programmatically.