# Resizable

Accessible resizable panel groups and layouts with keyboard support.

```tsx
import * as stylex from "@stylexjs/stylex"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { VStack } from "@/components/ui/stack"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  group: {
    maxWidth: "24rem",
    minHeight: "12.5rem",
    borderRadius: radius.lg,
    borderWidth: 1,
    borderStyle: "solid",
  },
  label: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 600 },
})

export default function ResizableDemo() {
  return (
    <ResizablePanelGroup orientation="horizontal" xstyle={styles.group}>
      <ResizablePanel defaultSize="50%">
        <VStack align="center" justify="center" padding={6} height="100%">
          <span {...stylex.props(styles.label)}>One</span>
        </VStack>
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel defaultSize="50%">
        <ResizablePanelGroup orientation="vertical">
          <ResizablePanel defaultSize="25%">
            <VStack align="center" justify="center" padding={6} height="100%">
              <span {...stylex.props(styles.label)}>Two</span>
            </VStack>
          </ResizablePanel>
          <ResizableHandle withHandle />
          <ResizablePanel defaultSize="75%">
            <VStack align="center" justify="center" padding={6} height="100%">
              <span {...stylex.props(styles.label)}>Three</span>
            </VStack>
          </ResizablePanel>
        </ResizablePanelGroup>
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install react-resizable-panels @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation). The handle also needs `interop.css` for its
   hover and drag colour.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/resizable.tsx"
   "use client"

   import { createContext, use } from "react"
   import * as ResizablePrimitive from "react-resizable-panels"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { color, duration, ease, radius, space } from "@/theme/tokens.stylex"

   type ResizableOrientation = "horizontal" | "vertical"

   const ResizableContext = createContext<ResizableOrientation>("horizontal")

   const styles = stylex.create({
     handle: {
       position: "relative",
       display: "flex",
       alignItems: "center",
       justifyContent: "center",
       color: { default: color.border, ":focus-visible": color.ring },
       backgroundColor: "currentColor",
       outlineStyle: { default: "none", ":focus-visible": "solid" },
       outlineWidth: 2,
       outlineColor: color.ring,
       transitionProperty: "color",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
     },
     grip: {
       flexShrink: 0,
       borderRadius: radius.full,
       borderWidth: 1,
       borderStyle: "solid",
       borderColor: color.border,
       backgroundColor: color.background,
     },
   })

   const handleStyles = stylex.create({
     horizontal: { width: 1 },
     vertical: { height: 1 },
   })

   const gripStyles = stylex.create({
     horizontal: { width: space.s1_5, height: space.s6 },
     vertical: { width: space.s6, height: space.s1_5 },
   })

   const resizableStyles = {
     handle: styles.handle,
     handles: handleStyles,
     grip: styles.grip,
     grips: gripStyles,
   }

   type StyleProps = {
     xstyle?: StyleXStyles
     className?: string
   }

   type ResizablePanelGroupProps = Omit<ResizablePrimitive.GroupProps, "className"> & StyleProps

   function ResizablePanelGroup({
     orientation = "horizontal",
     xstyle,
     className,
     style,
     children,
     ...props
   }: ResizablePanelGroupProps) {
     const sx = stylex.props(xstyle)

     return (
       <ResizableContext value={orientation}>
         <ResizablePrimitive.Group
           data-slot="resizable-panel-group"
           data-orientation={orientation}
           orientation={orientation}
           {...sx}
           {...props}
           className={clsx(sx.className, className)}
           style={{ ...sx.style, ...style }}
         >
           {children}
         </ResizablePrimitive.Group>
       </ResizableContext>
     )
   }

   type ResizablePanelProps = Omit<ResizablePrimitive.PanelProps, "className"> & StyleProps

   function ResizablePanel({ xstyle, className, style, ...props }: ResizablePanelProps) {
     const sx = stylex.props(xstyle)

     return (
       <ResizablePrimitive.Panel
         data-slot="resizable-panel"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
         style={{ ...sx.style, ...style }}
       />
     )
   }

   type ResizableHandleProps = Omit<ResizablePrimitive.SeparatorProps, "className"> &
     StyleProps & {
       withHandle?: boolean
     }

   function ResizableHandle({
     withHandle = false,
     xstyle,
     className,
     style,
     ...props
   }: ResizableHandleProps) {
     const orientation = use(ResizableContext)
     const sx = stylex.props(styles.handle, handleStyles[orientation], xstyle)

     return (
       <ResizablePrimitive.Separator
         data-slot="resizable-handle"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
         style={{ ...sx.style, ...style }}
       >
         {withHandle && (
           <div
             data-slot="resizable-handle-grip"
             {...stylex.props(styles.grip, gripStyles[orientation])}
           />
         )}
       </ResizablePrimitive.Separator>
     )
   }

   export {
     ResizableHandle,
     ResizablePanel,
     ResizablePanelGroup,
     resizableStyles,
     type ResizableOrientation,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
```

```tsx
<ResizablePanelGroup orientation="horizontal">
  <ResizablePanel>One</ResizablePanel>
  <ResizableHandle />
  <ResizablePanel>Two</ResizablePanel>
</ResizablePanelGroup>
```

A group is a flex row or column that fills its parent. Panels split it. A handle
between two panels can be dragged, takes focus and responds to the arrow keys.

```text
ResizablePanelGroup
├── ResizablePanel
├── ResizableHandle
└── ResizablePanel
```

> **Note**
>
> The group sets `width: 100%`, `height: 100%`, `display`, `flex-direction` and
> `overflow` inline, and an inline declaration beats any class. Give it a floor or
> a cap through `xstyle` (`minHeight`, `maxWidth`) and a fixed size through
> `style`.

Size a panel with `defaultSize`, `minSize` and `maxSize`. A number is pixels, a
bare string is a percentage, and a string with a unit is that unit.

```tsx
<ResizablePanel defaultSize="30%" minSize={200} maxSize="50%" />
```

> **Note**
>
> `defaultSize={50}` is 50 pixels, not half the group. Write `"50%"` or `"50"`
> for the percentage.

## Vertical

Use `orientation="vertical"` to stack the panels.

```tsx
import * as stylex from "@stylexjs/stylex"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { VStack } from "@/components/ui/stack"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  group: {
    maxWidth: "24rem",
    minHeight: "12.5rem",
    borderRadius: radius.lg,
    borderWidth: 1,
    borderStyle: "solid",
  },
  label: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 600 },
})

export default function ResizableVertical() {
  return (
    <ResizablePanelGroup orientation="vertical" xstyle={styles.group}>
      <ResizablePanel defaultSize="25%">
        <VStack align="center" justify="center" padding={6} height="100%">
          <span {...stylex.props(styles.label)}>Header</span>
        </VStack>
      </ResizablePanel>
      <ResizableHandle />
      <ResizablePanel defaultSize="75%">
        <VStack align="center" justify="center" padding={6} height="100%">
          <span {...stylex.props(styles.label)}>Content</span>
        </VStack>
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}
```

## Handle

Use the `withHandle` prop on `ResizableHandle` to draw a grip on the line.

```tsx
import * as stylex from "@stylexjs/stylex"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { VStack } from "@/components/ui/stack"
import { leading, radius, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  group: {
    maxWidth: "24rem",
    minHeight: "12.5rem",
    borderRadius: radius.lg,
    borderWidth: 1,
    borderStyle: "solid",
  },
  label: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 600 },
})

export default function ResizableHandleDemo() {
  return (
    <ResizablePanelGroup orientation="horizontal" xstyle={styles.group}>
      <ResizablePanel defaultSize="25%">
        <VStack align="center" justify="center" padding={6} height="100%">
          <span {...stylex.props(styles.label)}>Sidebar</span>
        </VStack>
      </ResizablePanel>
      <ResizableHandle withHandle />
      <ResizablePanel defaultSize="75%">
        <VStack align="center" justify="center" padding={6} height="100%">
          <span {...stylex.props(styles.label)}>Content</span>
        </VStack>
      </ResizablePanel>
    </ResizablePanelGroup>
  )
}
```

## API reference

Every part wraps the [react-resizable-panels](https://github.com/bvaughn/react-resizable-panels)
part of the same role and accepts its props: `Group`, `Panel` and `Separator`.
Each part also takes `xstyle` and `className`.

### ResizablePanelGroup

| Prop              | Type                                                             | Default        |
| ----------------- | ---------------------------------------------------------------- | -------------- |
| `orientation`     | `"horizontal" \| "vertical"`                                     | `"horizontal"` |
| `defaultLayout`   | `{ [panelId: string]: number }`                                  | —              |
| `onLayoutChanged` | `(layout: Layout, meta: { isUserInteraction: boolean }) => void` | —              |
| `disabled`        | `boolean`                                                        | `false`        |
| `disableCursor`   | `boolean`                                                        | `false`        |
| `groupRef`        | `Ref<GroupImperativeHandle>`                                     | —              |
| `id`              | `string \| number`                                               | —              |

`onLayoutChanged` fires once the pointer is released, so save a layout from that
callback. `onLayoutChange` fires on every pointer move. The library
sets the cursor from its own hit test for the whole page while a handle is
hovered or dragged; `disableCursor` turns that off.

### ResizablePanel

| Prop            | Type                                                  | Default |
| --------------- | ----------------------------------------------------- | ------- |
| `defaultSize`   | `number \| string`                                    | —       |
| `minSize`       | `number \| string`                                    | —       |
| `maxSize`       | `number \| string`                                    | —       |
| `collapsible`   | `boolean`                                             | `false` |
| `collapsedSize` | `number \| string`                                    | `0`     |
| `disabled`      | `boolean`                                             | `false` |
| `onResize`      | `(size: PanelSize, id, prevSize?: PanelSize) => void` | —       |
| `panelRef`      | `Ref<PanelImperativeHandle>`                          | —       |
| `id`            | `string \| number`                                    | —       |

The library renders a panel as two elements: an outer flex item it sizes, and an
inner box that scrolls. `className`, `style` and `xstyle` apply to the inner
box, so a style you pass cannot override the sizing.

A `collapsible` panel snaps to `collapsedSize` when it is dragged under its
`minSize`. `panelRef` reaches `collapse()`, `expand()` and `isCollapsed()`, so a
toggle button can call them. `usePanelRef` from `react-resizable-panels` returns
a ref of the right type.

### ResizableHandle

| Prop                 | Type      | Default |
| -------------------- | --------- | ------- |
| `withHandle`         | `boolean` | `false` |
| `disabled`           | `boolean` | `false` |
| `disableDoubleClick` | `boolean` | `false` |

Double-clicking a handle resets its panels to their default sizes unless
`disableDoubleClick` is set. A disabled handle cannot be dragged, but its
panels still move when a neighbour is resized; disable the panel too to pin it.

### resizableStyles

The style maps behind the handle, exported so the same line and grip can be put
on another element.

| Key       | Description                                                          |
| --------- | -------------------------------------------------------------------- |
| `handle`  | The line: centring flex box, `currentColor` fill, focus outline      |
| `handles` | One style per group orientation, the 1px width or height             |
| `grip`    | The flat pill: rounding, page fill and hairline                      |
| `grips`   | One style per group orientation, the pill's long side along the line |

### Data attributes

| Attribute                           | On                  | Effect                                                                            |
| ----------------------------------- | ------------------- | --------------------------------------------------------------------------------- |
| `data-slot="resizable-panel-group"` | the group           | Targets the group from a parent                                                   |
| `data-orientation`                  | the group           | The `orientation` prop                                                            |
| `data-slot="resizable-panel"`       | a panel's outer box | The flex item the library sizes                                                   |
| `data-slot="resizable-handle"`      | the separator       | Where `interop.css` keys the hover and drag colour                                |
| `data-slot="resizable-handle-grip"` | the grip            | Targets the pill                                                                  |
| `data-separator`                    | the separator       | The library writes `"inactive"`, `"hover"`, `"active"`, `"focus"` or `"disabled"` |
| `aria-orientation`                  | the separator       | Runs across the group, so `"vertical"` in a horizontal group                      |
| `data-group`, `data-panel`          | group and outer box | The library writes them, with the `id` as the value                               |
| `data-disabled`                     | a panel's outer box | The library writes it from `disabled`                                             |
