> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ResizeHandle

> Resize a panel with pointer dragging or keyboard controls.

export const StoryEmbed = ({storyId, title, height = 120}) => <>
    <Tabs>
      <Tab title="Light">
        <iframe title={`${title} (light)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:light`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
      <Tab title="Dark">
        <iframe title={`${title} (dark)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:dark`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
    </Tabs>
    <a href={`https://storybook.twenty.com/?path=/story/${storyId}`}>
      Open in Storybook
    </a>
  </>;

`ResizeHandle` manages a numeric size and exposes a focusable separator. The application applies that size to the panel; the handle does not resize another element automatically.

<StoryEmbed storyId="ui-layout-resizehandle--display" title="ResizeHandle example" height={600} />

## Usage

Use `edge` for a panel boundary, or `axis="y"` for inline height resizing and `axis="x"` for inline width resizing. Give the handle a specific accessible name and use `aria-controls` to identify the panel.

```tsx theme={null}
import { useId, useState } from 'react';
import { ResizeHandle } from 'twenty-ui/primitives/layout';
import { Card } from 'twenty-ui/primitives/surfaces';
import { Text } from 'twenty-ui/primitives/typography';

export const ResizableNotes = () => {
  const panelId = useId();
  const [height, setHeight] = useState(150);

  return (
    <Card.Root style={{ width: 300, maxWidth: '100%' }}>
      <Text
        id={panelId}
        style={{
          height,
          overflow: 'auto',
          padding: 16,
          boxSizing: 'border-box',
        }}
      >
        <Text
          style={{
            color: 'var(--t-font-color-primary)',
            fontWeight: 'var(--t-font-weight-medium)',
          }}
        >
          Record notes
        </Text>
        <Text style={{ marginTop: 8, lineHeight: 1.5 }}>
          Follow up with Morgan about the proposal. Share the revised timeline
          before Friday.
        </Text>
      </Text>
      <Text style={{ padding: 4 }}>
        <ResizeHandle
          axis="y"
          defaultValue={150}
          onValueChange={setHeight}
          min={50}
          max={500}
          aria-label="Resize notes height"
          aria-controls={panelId}
        />
      </Text>
    </Card.Root>
  );
};
```

The handle owns its value. `onValueChange` copies the latest size to the panel layout without controlling the handle. `defaultValue` only sets the initial size.

## Controlled state

```tsx theme={null}
import { useId, useState } from 'react';
import { ResizeHandle } from 'twenty-ui/primitives/layout';
import { Card } from 'twenty-ui/primitives/surfaces';
import { Text } from 'twenty-ui/primitives/typography';

export const ResizableNotes = () => {
  const panelId = useId();
  const [height, setHeight] = useState(150);

  return (
    <Card.Root style={{ width: 300, maxWidth: '100%' }}>
      <Text
        id={panelId}
        style={{
          height,
          overflow: 'auto',
          padding: 16,
          boxSizing: 'border-box',
        }}
      >
        <Text
          style={{
            color: 'var(--t-font-color-primary)',
            fontWeight: 'var(--t-font-weight-medium)',
          }}
        >
          Record notes
        </Text>
        <Text style={{ marginTop: 8, lineHeight: 1.5 }}>
          Follow up with Morgan about the proposal. Share the revised timeline
          before Friday.
        </Text>
      </Text>
      <Text style={{ padding: 4 }}>
        <ResizeHandle
          axis="y"
          value={height}
          onValueChange={setHeight}
          min={50}
          max={500}
          aria-label="Resize notes height"
          aria-controls={panelId}
        />
      </Text>
    </Card.Root>
  );
};
```

The application owns the value and can update it from other controls. Reflect live changes in `value`; use `onValueCommitted` to save the final size. Keep `value` and `defaultValue` separate and retain the same ownership mode throughout the component's lifetime.

## Panel edges and gaps

`edge="left"`, `"right"`, `"top"` or `"bottom"` determines both the axis and the physical increasing direction. Leftward movement grows a left edge, upward movement grows a top edge, and the opposite applies to right and bottom edges. This behavior stays the same in right-to-left content. Use either `edge` or `axis` and `direction`.

With `edge`, placement defaults to `"edge"`. Put the handle inside a panel with `position: 'relative'` to position it along that boundary. `placement="inline"` keeps it in normal flow. Use `placement="gap"` between sibling panels and set its width or height through `style` when the layout needs a gap. Gap handles have no visible grip by default.

```tsx theme={null}
import { useId, useState } from 'react';
import { ResizeHandle } from 'twenty-ui/primitives/layout';
import { Card } from 'twenty-ui/primitives/surfaces';
import { Text } from 'twenty-ui/primitives/typography';

export const ResizableDetails = ({
  saveWidth,
}: {
  saveWidth: (value: number) => void;
}) => {
  const detailsId = useId();
  const [width, setWidth] = useState(380);

  return (
    <div style={{ display: 'flex', height: 200 }}>
      <Card.Root style={{ flex: 1 }}>
        <Text>Activity</Text>
      </Card.Root>
      <ResizeHandle
        edge="left"
        placement="gap"
        value={width}
        onValueChange={setWidth}
        onValueCommitted={saveWidth}
        min={280}
        max={800}
        style={{ width: 12 }}
        aria-label="Resize details"
        aria-controls={detailsId}
      />
      <Card.Root id={detailsId} style={{ width }}>
        <Text>Details</Text>
      </Card.Root>
    </div>
  );
};
```

<StoryEmbed storyId="ui-layout-resizehandle-panels--documentation" title="ResizeHandle panel edge example" height={250} />

<StoryEmbed storyId="ui-layout-resizehandle-panels--gap" title="ResizeHandle panel gap example" height={520} />

The application owns the layout, saved size, collapse state and focus destination when collapsing removes the handle. Use `onActivate` for an optional collapse action.

## Keyboard and bounds

For `axis="y"`, Down increases the size and Up decreases it. For `axis="x"`, Right increases and Left decreases in left-to-right content; the directions reverse under [DirectionProvider](/ui/primitives/layout/direction-provider) with `direction="rtl"`.

Arrow keys use `step`, which defaults to `10`. Home moves to `min`, and End moves to `max`. Keys pressed with Alt, Control or Meta are ignored. Pointer dragging uses the pixel movement and is not snapped to `step`. Both interactions stay within the bounds. Pointer presses do not move focus; keyboard users reach the handle with Tab. `disabled` prevents resizing and removes the handle from the tab order.

The separator orientation describes the dividing edge: horizontal for height resizing and vertical for width resizing.

## Physical directions and zoom

Set `direction="normal"` or `direction="reverse"` when increasing size must follow a fixed physical direction, regardless of text direction. Normal increases with rightward or downward movement. Reverse increases with leftward or upward movement. Omit `direction` to retain the text-direction behavior described above.

`scale` accepts a positive zoom factor or a function returning one. The handle samples it at pointer down and divides pointer movement by that value. Keyboard steps remain unchanged.

## Gesture lifecycle

`onValueChange` reports live values. `onValueCommitted` reports the final value after a successful pointer drag or an effective keyboard change. Home, End and arrow keys commit immediately when they change the size.

`onResizeStart` receives the first bounded candidate after the pointer passes `dragThreshold`. The threshold defaults to `5` with `edge` and `0` otherwise, measured in physical pixels. `onResizeEnd` receives `{ cancelled, value }` for a started drag. Escape, pointer cancellation, lost pointer capture and unmounting cancel the gesture without committing; disabling the handle cancels it on its next pointer event. Cancellation calls `onValueChange` with the starting value, clamped to the current bounds. `onResizeEnd` receives that restored value.

Supply `onActivate` to handle a click without dragging, Enter or Space. Crossing the threshold prevents the following pointer click from activating, even when the pointer returns to its starting position.

Native handlers run before the handle's interaction. Calling `preventDefault()` in `onPointerDown` or `onKeyDown` skips that interaction. Preventing `onPointerMove` skips only that update and leaves the gesture active. Preventing `onPointerUp` cancels the drag, restores the starting bounded value and skips `onValueCommitted`. Pointer cancellation and lost capture still end the gesture.

## Custom handle

`ref`, native attributes, `className`, `style` and native event handlers target the separator root, a `div` by default. The ref does not target the resized panel or the visible grip.

`children` replaces the visible grip, and `children={null}` hides it; it does not replace the separator behavior. `render` can replace the root element, and the ref then targets that replacement. Custom wrappers must [forward the supplied props and ref](/ui/primitives/overview#custom-components) so focus, pointer capture, keyboard handlers, and accessibility attributes reach the same element.

## Props

<ParamField body="axis" type="&#x22;x&#x22; | &#x22;y&#x22;" default="y without edge">
  Pointer movement axis without `edge`: `y` for height or `x` for width.
</ParamField>

<ParamField body="defaultValue" type="number | (readonly string[] & number)" default="150">
  Initial size when the handle owns its state.
</ParamField>

<ParamField body="direction" type="&#x22;normal&#x22; | &#x22;reverse&#x22;">
  Physical increasing direction. Omit to follow the horizontal text direction; use `normal` or `reverse` for a fixed physical edge.
</ParamField>

<ParamField body="disabled" type="boolean" default="false">
  Disables resizing and removes the handle from the tab order.
</ParamField>

<ParamField body="dragThreshold" type="number" default="5 with edge; 0 otherwise">
  Physical pointer distance required before resizing starts. Movement below the threshold remains a click. Defaults to 5 with `edge` and 0 otherwise.
</ParamField>

<ParamField body="edge" type="&#x22;bottom&#x22; | &#x22;left&#x22; | &#x22;right&#x22; | &#x22;top&#x22;">
  Physical panel edge. Determines the movement axis and increasing direction, independently of text direction.
</ParamField>

<ParamField body="max" type="number" default="500">
  Maximum allowed size.
</ParamField>

<ParamField body="min" type="number" default="50">
  Minimum allowed size.
</ParamField>

<ParamField body="onActivate" type="(() => void)">
  Called for a click without dragging, Enter, or Space.
</ParamField>

<ParamField body="onResizeEnd" type="((details: { cancelled: boolean; value: number; }) => void)">
  Called after a started drag ends, with its final value and whether it was cancelled. Cancellation restores the starting value within the current bounds.
</ParamField>

<ParamField body="onResizeStart" type="((value: number) => void)">
  Called once with the first bounded drag value after the threshold is crossed.
</ParamField>

<ParamField body="onValueChange" type="((value: number) => void)">
  Called with the bounded size during pointer or keyboard interaction and with the restored value on cancellation.
</ParamField>

<ParamField body="onValueCommitted" type="((value: number) => void)">
  Called with the final size after a successful drag or an effective keyboard size change. Cancellation does not commit.
</ParamField>

<ParamField body="placement" type="&#x22;edge&#x22; | &#x22;gap&#x22; | &#x22;inline&#x22;" default="edge with edge; inline otherwise">
  Layout mode: `inline` stays in normal flow, `edge` positions the handle on the panel edge, and `gap` sits between panels. Defaults to `edge` when `edge` is set and `inline` otherwise.
</ParamField>

<ParamField body="render" type="ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>">
  Allows you to replace the component's HTML element
  with a different tag, or compose it with another component.

  Accepts a `ReactElement` or a function that returns the element to render.
</ParamField>

<ParamField body="scale" type="number | (() => number)" default="1">
  Pointer scale or resolver sampled at pointer down. Movement is divided by this positive scale; keyboard steps are unchanged.
</ParamField>

<ParamField body="step" type="number" default="10">
  Keyboard arrow increment. Pointer movement uses the pixel delta.
</ParamField>

<ParamField body="value" type="number">
  Current controlled size. Update it in `onValueChange` and apply it to the element being resized.
</ParamField>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.