npm package: @isbatak/react-resizable-panels
Description: React components for resizable panel groups/layouts
Author: Brian Vaughn
Latest version: 3.1.0-beta.4

# react-resizable-panels

React components for resizable panel groups/layouts

```jsx
import { Panel, PanelGroup, PanelResizeHandle } from "@isbatak/react-resizable-panels";

<PanelGroup autoSaveId="example" direction="horizontal">
  <Panel defaultSize={25}>
    <SourcesExplorer />
  </Panel>
  <PanelResizeHandle />
  <Panel>
    <SourceViewer />
  </Panel>
  <PanelResizeHandle />
  <Panel defaultSize={25}>
    <Console />
  </Panel>
</PanelGroup>;
```

## If you like this project, 🎉 [become a sponsor](https://github.com/sponsors/bvaughn/) or ☕ [buy me a coffee](http://givebrian.coffee/)

## Props

### `PanelGroup`

| prop         | type                         | description                                                      |
| :----------- | :--------------------------- | :--------------------------------------------------------------- |
| `autoSaveId` | `?string`                    | Unique id used to auto-save group arrangement via `localStorage` |
| `children`   | `ReactNode`                  | Arbitrary React element(s)                                       |
| `className`  | `?string`                    | Class name to attach to root element                             |
| `direction`  | `"horizontal" \| "vertical"` | Group orientation                                                |
| `id`         | `?string`                    | Group id; falls back to `useId` when not provided                |
| `onLayout`   | `?(sizes: number[]) => void` | Called when group layout changes                                 |
| `storage`    | `?PanelGroupStorage`         | Custom storage API; defaults to `localStorage` <sup>1</sup>      |
| `style`      | `?CSSProperties`             | CSS style to attach to root element                              |
| `tagName`    | `?string = "div"`            | HTML element tag name for root element                           |

<sup>1</sup>: Storage API must define the following _synchronous_ methods:

- `getItem: (name:string) => string`
- `setItem: (name: string, value: string) => void`

`PanelGroup` components also expose an imperative API for manual resizing:
| method                        | description                                                      |
| :---------------------------- | :--------------------------------------------------------------- |
| `getId(): string`             | Gets the panel group's ID.                                       |
| `getLayout(): number[]`       | Gets the panel group's current _layout_ (`[1 - 100, ...]`).      |
| `setLayout(layout: number[])` | Resize panel group to the specified _layout_ (`[1 - 100, ...]`). |

### `Panel`

| prop            | type                      | description                                                                                   |
| :-------------- | :------------------------ | :-------------------------------------------------------------------------------------------- |
| `children`      | `ReactNode`               | Arbitrary React element(s)                                                                    |
| `className`     | `?string`                 | Class name to attach to root element                                                          |
| `collapsedSize` | `?number=0`               | Panel should collapse to this size                                                            |
| `collapsible`   | `?boolean=false`          | Panel should collapse when resized beyond its `minSize`                                       |
| `defaultSize`   | `?number`                 | Initial size of panel (numeric value between 1-100)                                           |
| `id`            | `?string`                 | Panel id (unique within group); falls back to `useId` when not provided                       |
| `maxSize`       | `?number = 100`           | Maximum allowable size of panel (numeric value between 1-100); defaults to `100`              |
| `minSize`       | `?number = 10`            | Minimum allowable size of panel (numeric value between 1-100); defaults to `10`               |
| `onCollapse`    | `?() => void`             | Called when panel is collapsed                                                                |
| `onExpand`      | `?() => void`             | Called when panel is expanded                                                                 |
| `onResize`      | `?(size: number) => void` | Called when panel is resized; `size` parameter is a numeric value between 1-100. <sup>1</sup> |
| `order`         | `?number`                 | Order of panel within group; required for groups with conditionally rendered panels           |
| `style`         | `?CSSProperties`          | CSS style to attach to root element                                                           |
| `tagName`       | `?string = "div"`         | HTML element tag name for root element                                                        |

<sup>1</sup>: If any `Panel` has an `onResize` callback, the `order` prop should be provided for all `Panel`s.

`Panel` components also expose an imperative API for manual resizing:
| method                   | description                                                                        |
| :----------------------- | :--------------------------------------------------------------------------------- |
| `collapse()`             | If panel is `collapsible`, collapse it fully.                                      |
| `expand()`               | If panel is currently _collapsed_, expand it to its most recent size.              |
| `getId(): string`        | Gets the ID of the panel.                                                          |
| `getSize(): number`      | Gets the current size of the panel as a percentage (`1 - 100`).                    |
| `isCollapsed(): boolean` | Returns `true` if the panel is currently _collapsed_ (`size === 0`).               |
| `isExpanded(): boolean`  | Returns `true` if the panel is currently _not collapsed_ (`!isCollapsed()`).       |
| `resize(size: number)`   | Resize panel to the specified _percentage_ (`1 - 100`).                            |

### `PanelResizeHandle`

| prop             | type                                          | description                                                                     |
| :--------------- | :-------------------------------------------- | :------------------------------------------------------------------------------ |
| `children`       | `?ReactNode`                                  | Custom drag UI; can be any arbitrary React element(s)                           |
| `className`      | `?string`                                     | Class name to attach to root element                                            |
| `hitAreaMargins` | `?{ coarse: number = 15; fine: number = 5; }` | Allow this much margin when determining resizable handle hit detection          |
| `disabled`       | `?boolean`                                    | Disable drag handle                                                             |
| `id`             | `?string`                                     | Resize handle id (unique within group); falls back to `useId` when not provided |
| `onDragging`     | `?(isDragging: boolean) => void`              | Called when group layout changes                                                |
| `style`          | `?CSSProperties`                              | CSS style to attach to root element                                             |
| `tagName`        | `?string = "div"`                             | HTML element tag name for root element                                          |


### `PersistScript`

| prop         | type          | description                                                                                   |
| :----------- | :------------ | :-------------------------------------------------------------------------------------------- |

---

## FAQ

### Can panel sizes be specified in pixels?

No. Pixel-based constraints [added significant complexity](https://github.com/bvaughn/react-resizable-panels/pull/176) to the initialization and validation logic and so I've decided not to support them. You may be able to implement a version of this yourself following [a pattern like this](https://github.com/bvaughn/react-resizable-panels/issues/46#issuecomment-1368108416) but it is not officially supported by this library.

### How can I fix layout/sizing problems with conditionally rendered panels?

The `Panel` API doesn't _require_ `id` and `order` props because they aren't necessary for static layouts. When panels are conditionally rendered though, it's best to supply these values.

```tsx
<PanelGroup direction="horizontal">
  {renderSideBar && (
    <>
      <Panel id="sidebar" minSize={25} order={1}>
        <Sidebar />
      </Panel>
      <PanelResizeHandle />
    </>
  )}
  <Panel minSize={25} order={2}>
    <Main />
  </Panel>
</PanelGroup>
```

### Can I attach a ref to the DOM elements?

No. I think exposing two refs (one for the component's imperative API and one for a DOM element) would be awkward. This library does export several utility methods for accessing the underlying DOM elements though. For example:

```tsx
import {
  getPanelElement,
  getPanelGroupElement,
  getResizeHandleElement,
  Panel,
  PanelGroup,
  PanelResizeHandle,
} from "@isbatak/react-resizable-panels";

export function Example() {
  const refs = useRef();

  useEffect(() => {
    const groupElement = getPanelGroupElement("group");
    const leftPanelElement = getPanelElement("left-panel");
    const rightPanelElement = getPanelElement("right-panel");
    const resizeHandleElement = getResizeHandleElement("resize-handle");

    // If you want to, you can store them in a ref to pass around
    refs.current = {
      groupElement,
      leftPanelElement,
      rightPanelElement,
      resizeHandleElement,
    };
  }, []);

  return (
    <PanelGroup direction="horizontal" id="group">
      <Panel id="left-panel">{/* ... */}</Panel>
      <PanelResizeHandle id="resize-handle" />
      <Panel id="right-panel">{/* ... */}</Panel>
    </PanelGroup>
  );
}
```

### Why don't I see any resize UI?

This likely means that you haven't applied any CSS to style the resize handles. By default, a resize handle is just an empty DOM element. To add styling, use the `className` or `style` props:

```tsx
// Tailwind example
<PanelResizeHandle className="w-2 bg-blue-800" />
```

### Can panel sizes be persistent?

Yes. Panel groups with an `autoSaveId` prop will automatically save and restore their layouts on mount.

### How can I use persistent layouts with SSR?

By default, this library uses `localStorage` to persist layouts. With server rendering, this can cause a flicker when the default layout (rendered on the server) is replaced with the persisted layout (in `localStorage`). There are two ways to avoid this flicker:

#### Option 1: Using `PersistScript`

The `PersistScript` component synchronously applies the persisted layout before React hydration, eliminating layout flicker.
It injects CSS variable rules into an adopted stylesheet based on the saved layout in `localStorage`.

```tsx
"use client";

import { Panel, PanelGroup, PanelResizeHandle, PersistScript } from "react-resizable-panels";

export function ClientComponent() {
  return (
    <>
      <PersistScript/>
      <PanelGroup direction="horizontal" autoSaveId="my-layout">
        <Panel defaultSize={33} >
          {/* Panel content */}
        </Panel>
        <PanelResizeHandle />
        <Panel defaultSize={67}>
          {/* Panel content */}
        </Panel>
      </PanelGroup>
    </>
  );
}
```

Here's the example of injected CSS variables:

```css
:root {
    --panel-my-layout-1-size: 60.000;
    --panel-my-layout-2-size: 40.000;
}
```

> [!NOTE]
> A working example is available in [`examples/nextjs`](https://github.com/bvaughn/react-resizable-panels/tree/main/examples/nextjs).

#### Option 2: Cookie-based persistence

Alternatively, you can persist the layout with cookies and pass it from the server to the client.

**Server component:**

```tsx
import ResizablePanels from "@/app/ResizablePanels";
import { cookies } from "next/headers";

export function ServerComponent() {
  const layout = cookies().get("react-resizable-panels:layout");

  let defaultLayout;
  if (layout) {
    defaultLayout = JSON.parse(layout.value);
  }

  return <ClientComponent defaultLayout={defaultLayout} />;
}
```

**Client component:**

```tsx
"use client";

import { Panel, PanelGroup, PanelResizeHandle } from "@isbatak/react-resizable-panels";

export function ClientComponent({
  defaultLayout = [33, 67],
}: {
  defaultLayout: number[] | undefined;
}) {
  const onLayout = (sizes: number[]) => {
    document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
  };

  return (
    <PanelGroup direction="horizontal" onLayout={onLayout}>
      <Panel defaultSize={defaultLayout[0]}>{/* ... */}</Panel>
      <PanelResizeHandle className="w-2 bg-blue-800" />
      <Panel defaultSize={defaultLayout[1]}>{/* ... */}</Panel>
    </PanelGroup>
  );
}
```

> [!NOTE]
> Be sure to specify a `defaultSize` prop for **every** `Panel` component to avoid layout flicker.

A demo of this is available [here](https://github.com/bvaughn/react-resizable-panels-demo-ssr).

#### How can I set the [CSP `"nonce"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) attribute?

```js
import { setNonce } from "@isbatak/react-resizable-panels";

setNonce("your-nonce-value-here");
```

#### How can I disable global cursor styles?

```js
import { disableGlobalCursorStyles } from "@isbatak/react-resizable-panels";

disableGlobalCursorStyles();
```

#### How can I override the global cursor styles?

```js
import { customizeGlobalCursorStyles, type CustomCursorStyleConfig } from "@isbatak/react-resizable-panels";

function customCursor({ isPointerDown }: CustomCursorStyleConfig) {
  return isPointerDown ? "grabbing" : "grab";
}

customizeGlobalCursorStyles(customCursor);
```