# How to Implement Resizable Panels Using the Astryx Resizable Component

> Learn to implement resizable panels with Astryx's useResizable hook and ResizeHandle component. Add drag-to-resize to any layout with minimal code.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-05

---

**Astryx provides a hook-first API for building resizable panels through the `useResizable` hook and `ResizeHandle` component, letting you add drag-to-resize behavior to any existing layout component with minimal markup.**

The Astryx design system offers a lightweight, accessible solution for resizable panels that integrates directly with layout primitives like `SideNav` and `LayoutPanel`. Rather than wrapping components in heavy container elements, you connect resize behavior through a shared props object—keeping your React tree clean and your bundle size small. This guide covers the complete implementation based on the Facebook Astryx source code.

## Core Architecture of Astryx Resizable Panels

Astryx resizable panels rely on three coordinated pieces:

- **`useResizable`** – A React hook in [[`useResizable.ts`](https://github.com/facebook/astryx/blob/main/useResizable.ts)](https://github.com/facebook/astryx/blob/main/packages/core/src/Resizable/useResizable.ts) that manages size state, constraints, persistence, and control methods.
- **`ResizeHandle`** – A visual component in [[`ResizeHandle.tsx`](https://github.com/facebook/astryx/blob/main/ResizeHandle.tsx)](https://github.com/facebook/astryx/blob/main/packages/core/src/Resizable/ResizeHandle.tsx) that renders the draggable pill-grip and handles mouse/keyboard interactions.
- **Layout component integration** – Standard Astryx components accept a `resizable` prop to receive sizing instructions from the hook.

The hook performs all state calculations, leaving components to render purely based on the provided `props`.

## Configuring the useResizable Hook

Before rendering, define your resizable region's behavior through the hook's configuration object:

| Property | Type | Purpose |
|----------|------|---------|
| `defaultWidth` / `defaultHeight` | `number` | Initial size in pixels (use width for horizontal, height for vertical splits). |
| `minWidth` / `minHeight` | `number` | Lower bound the user cannot drag below. |
| `maxWidth` / `maxHeight` | `number` | Upper bound the user cannot drag above. |
| `collapsible` | `boolean` | Whether the panel can collapse to its `collapsedSize`. |
| `collapsedSize` | `number` | Width/height when collapsed (default: `0`). |
| `snaps` | `number[]` | Discrete widths/heights the panel jumps to during drag. |
| `autoSaveId` | `string` | Unique key for persisting size in `localStorage` (prefixed with `astryx-resizable:`). |
| `direction` | `'horizontal' \| 'vertical'` | Orientation for multi-region layouts. |

The hook returns an object containing:

- `size` – Current numeric size in pixels.
- `isCollapsed` – Boolean collapse state.
- `collapse()` / `expand()` – Programmatic control methods.
- `resize(newSize)` – Direct size setter (respects constraints).
- `props` – Object to spread onto `resizable` prop of layout components and `ResizeHandle`.

## Basic Two-Pane Horizontal Layout

This example creates a collapsible sidebar with persistent size:

```tsx
import { useResizable, SideNav, LayoutPanel, ResizeHandle } from '@astryxdesign/core';

export function TwoPaneExample() {
  const { props, collapse, expand } = useResizable({
    defaultWidth: 260,
    minWidth: 180,
    maxWidth: 480,
    collapsible: true,
    collapsedSize: 40,
    snaps: [180, 260, 360],
    autoSaveId: 'example-sidebar',
  });

  return (
    <div style={{ display: 'flex', height: '100vh' }}>
      <SideNav resizable={props}>
        <ResizeHandle direction="horizontal" resizable={props} />
        <nav>Sidebar content</nav>
      </SideNav>

      <LayoutPanel style={{ flex: 1 }}>
        <h1>Main area</h1>
        <button onClick={collapse}>Hide sidebar</button>
        <button onClick={expand}>Show sidebar</button>
      </LayoutPanel>
    </div>
  );
}

```

**How this works:**

- `SideNav` receives `resizable={props}` and applies `props._size` to its container width.
- `ResizeHandle` receives the same `props` object, linking drag gestures to the hook's state reducer.
- The `direction="horizontal"` prop tells the handle to drag along the x-axis and render a vertical pill-grip.

## Multi-Region Vertical Layout

For complex arrangements with multiple resizable boundaries, pass a `regions` map to `useResizable`:

```tsx
import { useResizable, LayoutPanel, ResizeHandle } from '@astryxdesign/core';

export function ThreePaneExample() {
  const regions = useResizable({
    direction: 'vertical',
    regions: {
      left:   { defaultSize: 200, minSizePx: 150, collapsible: true },
      center: { defaultSize: 400, minSizePx: 300 },
      right:  { defaultSize: 200, minSizePx: 150, collapsible: true },
    },
    autoSaveId: 'three-pane',
  });

  return (
    <div style={{ display: 'flex', height: '100vh' }}>
      <LayoutPanel resizable={regions.left.props}>
        <ResizeHandle direction="vertical" resizable={regions.left.props} />
        <div>Left content</div>
      </LayoutPanel>

      <LayoutPanel resizable={regions.center.props}>
        <ResizeHandle direction="vertical" resizable={regions.center.props} />
        <div>Center content</div>
      </LayoutPanel>

      <LayoutPanel resizable={regions.right.props}>
        <div>Right content</div>
      </LayoutPanel>
    </div>
  );
}

```

**Key implementation details:**

- Each region receives isolated state—resizing the left pane does not affect the center pane's stored default.
- Only two `ResizeHandle` components are needed for three panes; each handle controls the boundary between adjacent regions.
- The `direction="vertical"` prop configures the handle for y-axis dragging with a horizontal pill-grip.

## Adding Snap Points and Persistence

Combine `snaps` with `autoSaveId` for a polished production experience:

```tsx
const { props, isCollapsed, size } = useResizable({
  defaultWidth: 260,
  minWidth: 180,
  maxWidth: 480,
  snaps: [180, 260, 360, 480],
  autoSaveId: 'project-nav-panel',
});

// Display current state
<span>{isCollapsed ? 'Collapsed' : `${size}px wide`}</span>

```

The hook automatically:

1. Reads saved size from `localStorage` on mount (key: `astryx-resizable:project-nav-panel`).
2. Applies snap logic during drag—when the cursor approaches within 10px of a snap point, the panel jumps to that width.
3. Writes updated size after every `resize` or `collapse` event.

## Accessibility and Keyboard Support

The `ResizeHandle` component in Astryx provides full keyboard accessibility out of the box:

- **Arrow keys** – Resize by 10px increments (Shift + Arrow for 50px).
- **Home** – Jump to minimum size.
- **End** – Jump to maximum size.
- **Enter** – Toggle collapse/expand (when `collapsible: true`).

Screen reader announcements for size changes are handled through the `props` object, which includes appropriate `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes.

## Programmatic Control Methods

The hook exposes methods for non-drag interactions:

| Method | Behavior |
|--------|----------|
| `collapse()` | Animates to `collapsedSize` and sets `isCollapsed: true`. |
| `expand()` | Restores to the pre-collapse size or `defaultWidth` if never expanded. |
| `resize(targetSize)` | Immediately sets size, clamped to `minWidth`/`maxWidth`. |
| `toggle()` | Switches between collapsed and expanded states. |

Use these to build UI controls like collapse buttons, keyboard shortcuts, or responsive breakpoints that adjust panel sizes automatically.

## Resizable Panel Best Practices

Based on the Astryx source in [`Resizable.doc.mjs`](https://github.com/facebook/astryx/blob/main/packages/core/src/Resizable/Resizable.doc.mjs):

- **Always provide `minWidth`/`minHeight`** – Prevents users from collapsing panels to unusable sizes unless explicitly intended.
- **Use `autoSaveId` for persistent UI** – Navigation panels, sidebars, and tool palettes benefit from remembering user preferences.
- **Place `ResizeHandle` as first child** – Ensures the handle renders above content and receives proper z-index stacking.
- **Match `direction` prop to layout** – Mismatching the handle direction with the actual resize axis produces broken interactions.

## Summary

- **Hook-first design** – `useResizable` in [`packages/core/src/Resizable/useResizable.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Resizable/useResizable.ts) centralizes all resizable panel state.
- **Props-based integration** – Pass the hook's `props` object to any Astryx layout component's `resizable` prop and to `ResizeHandle`.
- **Minimal DOM overhead** – No wrapper elements required; existing components gain resize behavior through props alone.
- **Built-in accessibility** – Keyboard navigation, screen reader support, and focus management handled automatically.
- **Persistence ready** – `autoSaveId` enables localStorage integration with no additional code.

## Frequently Asked Questions

### How do I prevent a resizable panel from collapsing completely?

Omit the `collapsible: true` option from your `useResizable` config, or set `collapsedSize` equal to your `minWidth` value. Without `collapsible`, the hook ignores collapse gestures and the `collapse()` method becomes a no-op.

### Can I use Astryx resizable panels without SideNav or LayoutPanel?

Yes—any component that accepts a `resizable` prop can participate. The hook's `props` object contains `_size` and styling instructions that standard Astryx layout primitives understand. For custom components, read `props.style` or `props._size` directly in your component's render logic.

### Does Astryx support nested resizable regions?

Astryx supports arbitrary nesting: a horizontal split can contain a panel that itself contains a vertical split. Each `useResizable` call manages its own state independently. Ensure unique `autoSaveId` values for each nested region to prevent localStorage collisions.

### What happens if localStorage is unavailable or cleared?

The hook gracefully falls back to `defaultWidth` or `defaultHeight`. No errors are thrown, and the panel initializes at its configured default size. The next successful resize re-creates the localStorage entry.