# useSplitter
Package: @mantine/hooks
Import: import { useSplitter } from '@mantine/hooks';

## Usage

`use-splitter` hook provides resizable split-pane functionality. It handles pointer drag
on resize handles, keyboard navigation following the WAI-ARIA Window Splitter pattern,
collapsible panels and min/max constraints.

```tsx
import React from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

const colors = ['var(--mantine-color-blue-filled)', 'var(--mantine-color-teal-filled)'];
const labels = ['Panel A', 'Panel B'];

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 50, min: 20 },
      { defaultSize: 50, min: 20 },
    ],
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                width: 4,
                flexShrink: 0,
                cursor: 'col-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 8,
                  height: 40,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixVerticalIcon />
              </div>
            </div>
          )}
          <div
            style={{
              width: `${size}%`,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
              gap: 2,
            }}
          >
            {labels[i]} ({Math.round(size as number)}%)
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


## CSS units

`defaultSize`, `min` and `max` accept CSS units. A number or `%` string is a flexible size
that shares the leftover space, a `px` or `rem` string is a fixed size that does not change
when the container is resized. Sizes are reported in the unit they were declared in:
a `'240px'` pane stays `'240px'` in `sizes` and `onSizeChange`.

```tsx
import React from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { SplitterPaneSize, useSplitter } from '@mantine/hooks';

const colors = ['var(--mantine-color-blue-filled)', 'var(--mantine-color-teal-filled)'];

function getPaneStyle(size: SplitterPaneSize): React.CSSProperties {
  if (typeof size === 'string' && (size.endsWith('px') || size.endsWith('rem'))) {
    return { flexGrow: 0, flexShrink: 1, flexBasis: size };
  }
  const weight = typeof size === 'number' ? size : parseFloat(size);
  return { flexGrow: weight, flexShrink: 1, flexBasis: 0 };
}

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: '240px', min: '160px', max: '50%' },
      { defaultSize: 100 },
    ],
  });

  const labels = ['Fixed 240px sidebar', 'Flexible content'];

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                width: 4,
                flexShrink: 0,
                cursor: 'col-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 8,
                  height: 40,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixVerticalIcon />
              </div>
            </div>
          )}
          <div
            style={{
              ...getPaneStyle(size),
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
            }}
          >
            {labels[i]}
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


## Vertical orientation

Set `orientation: 'vertical'` to create a vertical split layout.
Keyboard navigation uses ArrowUp/ArrowDown instead of ArrowLeft/ArrowRight:

```tsx
import React from 'react';
import { DotsSixIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

const colors = ['var(--mantine-color-blue-filled)', 'var(--mantine-color-teal-filled)'];
const labels = ['Panel A', 'Panel B'];

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 50, min: 20 },
      { defaultSize: 50, min: 20 },
    ],
    orientation: 'vertical',
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        flexDirection: 'column',
        height: 300,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                height: 4,
                flexShrink: 0,
                cursor: 'row-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 40,
                  height: 8,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixIcon />
              </div>
            </div>
          )}
          <div
            style={{
              height: `${size}%`,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
              gap: 2,
            }}
          >
            {labels[i]} ({Math.round(size as number)}%)
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


## Collapsible panels

Set `collapsible: true` on a panel to allow it to collapse to zero size.
When dragged below the `collapseThreshold` (defaults to `min`), the panel
snaps to 0%. Use `collapse()`, `expand()` and `toggleCollapse()` for
programmatic control:

```tsx
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { Button, Group } from '@mantine/core';
import { useSplitter } from '@mantine/hooks';

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 30, collapsible: true, min: 15 },
      { defaultSize: 70, min: 20 },
    ],
  });

  return (
    <>
      <div
        ref={splitter.ref}
        style={{
          display: 'flex',
          height: 200,
          borderRadius: 'var(--mantine-radius-md)',
          overflow: 'hidden',
        }}
      >
        <div
          style={{
            width: `${splitter.sizes[0]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-blue-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            overflow: 'hidden',
            gap: 2,
          }}
        >
          {!splitter.collapsed[0] && `Panel A (${Math.round(splitter.sizes[0] as number)}%)`}
        </div>
        <div
          {...splitter.getHandleProps({ index: 0 })}
          style={{
            width: 4,
            flexShrink: 0,
            cursor: 'col-resize',
            touchAction: 'none',
            backgroundColor: 'var(--mantine-color-default-border)',
            position: 'relative',
          }}
        >
          <div
            style={{
              position: 'absolute',
              top: '50%',
              left: '50%',
              transform: 'translate(-50%, -50%)',
              width: 8,
              height: 40,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              borderRadius: 'var(--mantine-radius-xs)',
              backgroundColor: 'var(--mantine-color-default)',
              border: '1px solid var(--mantine-color-default-border)',
              color: 'var(--mantine-color-dimmed)',
            }}
          >
            <DotsSixVerticalIcon />
          </div>
        </div>
        <div
          style={{
            width: `${splitter.sizes[1]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-teal-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            gap: 2,
          }}
        >
          Panel B ({Math.round(splitter.sizes[1] as number)}%)
        </div>
      </div>
      <Group mt="md">
        <Button size="xs" onClick={() => splitter.toggleCollapse(0)}>
          {splitter.collapsed[0] ? 'Expand Panel A' : 'Collapse Panel A'}
        </Button>
      </Group>
    </>
  );
}
```


## Multiple panels

The hook supports any number of panels. Each handle controls the boundary
between its two adjacent panels:

```tsx
import React from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

const colors = [
  'var(--mantine-color-blue-filled)',
  'var(--mantine-color-teal-filled)',
  'var(--mantine-color-grape-filled)',
];
const labels = ['Panel A', 'Panel B', 'Panel C'];

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 25, min: 10 },
      { defaultSize: 50, min: 20 },
      { defaultSize: 25, min: 10 },
    ],
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                width: 4,
                flexShrink: 0,
                cursor: 'col-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 8,
                  height: 40,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixVerticalIcon />
              </div>
            </div>
          )}
          <div
            style={{
              width: `${size}%`,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
              gap: 2,
            }}
          >
            {labels[i]} ({Math.round(size as number)}%)
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


## Redistribute

By default, each handle only affects its two adjacent panels: when a neighbor panel
is at its minimum, the handle stops. Use the `redistribute` option to borrow
space from panels further away.

### Nearest

`redistribute: 'nearest'` takes space from the nearest panel in the drag direction first,
then moves to the next one if more space is needed. Drag the first handle to
the right to see it:

```tsx
import React from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

const colors = [
  'var(--mantine-color-blue-filled)',
  'var(--mantine-color-teal-filled)',
  'var(--mantine-color-grape-filled)',
  'var(--mantine-color-orange-filled)',
];
const labels = ['Panel A', 'Panel B', 'Panel C', 'Panel D'];

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 25, min: 10 },
      { defaultSize: 25, min: 20 },
      { defaultSize: 25, min: 10 },
      { defaultSize: 25, min: 10 },
    ],
    redistribute: 'nearest',
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                width: 4,
                flexShrink: 0,
                cursor: 'col-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 8,
                  height: 40,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixVerticalIcon />
              </div>
            </div>
          )}
          <div
            style={{
              width: `${size}%`,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
              gap: 2,
            }}
          >
            {labels[i]} ({Math.round(size as number)}%)
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


### Equal

`redistribute: 'equal'` takes space equally from all panels in the
drag direction, respecting the minimum size of each panel:

```tsx
import React from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

const colors = [
  'var(--mantine-color-blue-filled)',
  'var(--mantine-color-teal-filled)',
  'var(--mantine-color-grape-filled)',
  'var(--mantine-color-orange-filled)',
];
const labels = ['Panel A', 'Panel B', 'Panel C', 'Panel D'];

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 25, min: 10 },
      { defaultSize: 25, min: 20 },
      { defaultSize: 25, min: 10 },
      { defaultSize: 25, min: 10 },
    ],
    redistribute: 'equal',
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      {splitter.sizes.map((size, i) => (
        <React.Fragment key={i}>
          {i > 0 && (
            <div
              {...splitter.getHandleProps({ index: i - 1 })}
              style={{
                width: 4,
                flexShrink: 0,
                cursor: 'col-resize',
                touchAction: 'none',
                backgroundColor: 'var(--mantine-color-default-border)',
                position: 'relative',
              }}
            >
              <div
                style={{
                  position: 'absolute',
                  top: '50%',
                  left: '50%',
                  transform: 'translate(-50%, -50%)',
                  width: 8,
                  height: 40,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 'var(--mantine-radius-xs)',
                  backgroundColor: 'var(--mantine-color-default)',
                  border: '1px solid var(--mantine-color-default-border)',
                  color: 'var(--mantine-color-dimmed)',
                }}
              >
                <DotsSixVerticalIcon />
              </div>
            </div>
          )}
          <div
            style={{
              width: `${size}%`,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              backgroundColor: colors[i],
              color: 'var(--mantine-color-white)',
              fontWeight: 500,
              whiteSpace: 'nowrap',
              gap: 2,
            }}
          >
            {labels[i]} ({Math.round(size as number)}%)
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}
```


### Custom function

Pass a function to `redistribute` to control how space is borrowed.
The function receives `{ sizes, panels, handleIndex, delta }` and must return
a new sizes array. This example always borrows from the last panel when growing
and from the first panel when shrinking:

```tsx
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter, UseSplitterRedistributeInput } from '@mantine/hooks';

// Custom strategy: only take from the last panel, leave others untouched
function redistributeFromLast(input: UseSplitterRedistributeInput) {
  const { sizes, panels, handleIndex, delta } = input;
  const result = [...sizes];

  if (delta > 0) {
    const lastIdx = result.length - 1;
    const minLast = panels[lastIdx].min ?? 0;
    const maxGrow = (panels[handleIndex].max ?? 100) - result[handleIndex];
    const canTake = result[lastIdx] - minLast;
    const take = Math.min(delta, maxGrow, canTake);
    result[handleIndex] += take;
    result[lastIdx] -= take;
  } else {
    const firstIdx = 0;
    const minFirst = panels[firstIdx].min ?? 0;
    const growIdx = handleIndex + 1;
    const maxGrow = (panels[growIdx].max ?? 100) - result[growIdx];
    const canTake = result[firstIdx] - minFirst;
    const take = Math.min(Math.abs(delta), maxGrow, canTake);
    result[growIdx] += take;
    result[firstIdx] -= take;
  }

  return result;
}

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 25, min: 10 },
      { defaultSize: 25, min: 20 },
      { defaultSize: 50, min: 10 },
    ],
    redistribute: redistributeFromLast,
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          width: `${splitter.sizes[0]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-blue-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel A ({Math.round(splitter.sizes[0] as number)}%)
      </div>
      <div
        {...splitter.getHandleProps({ index: 0 })}
        style={{
          width: 4,
          flexShrink: 0,
          cursor: 'col-resize',
          touchAction: 'none',
          backgroundColor: 'var(--mantine-color-default-border)',
          position: 'relative',
        }}
      >
        <div
          style={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: 8,
            height: 40,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: 'var(--mantine-radius-xs)',
            backgroundColor: 'var(--mantine-color-default)',
            border: '1px solid var(--mantine-color-default-border)',
            color: 'var(--mantine-color-dimmed)',
          }}
        >
          <DotsSixVerticalIcon />
        </div>
      </div>
      <div
        style={{
          width: `${splitter.sizes[1]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-teal-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel B ({Math.round(splitter.sizes[1] as number)}%)
      </div>
      <div
        {...splitter.getHandleProps({ index: 1 })}
        style={{
          width: 4,
          flexShrink: 0,
          cursor: 'col-resize',
          touchAction: 'none',
          backgroundColor: 'var(--mantine-color-default-border)',
          position: 'relative',
        }}
      >
        <div
          style={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: 8,
            height: 40,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: 'var(--mantine-radius-xs)',
            backgroundColor: 'var(--mantine-color-default)',
            border: '1px solid var(--mantine-color-default-border)',
            color: 'var(--mantine-color-dimmed)',
          }}
        >
          <DotsSixVerticalIcon />
        </div>
      </div>
      <div
        style={{
          width: `${splitter.sizes[2]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-grape-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel C ({Math.round(splitter.sizes[2] as number)}%)
      </div>
    </div>
  );
}
```


## Grip only handle

The drag handle can be a small grip button instead of a full-height bar.
In this example, only the grip icon is draggable, the line between panels is not
interactive:

```tsx
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

function Demo() {
  const splitter = useSplitter({
    panels: [
      { defaultSize: 50, min: 20 },
      { defaultSize: 50, min: 20 },
    ],
  });

  return (
    <div
      ref={splitter.ref}
      style={{
        display: 'flex',
        height: 200,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          width: `${splitter.sizes[0]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-blue-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel A ({Math.round(splitter.sizes[0] as number)}%)
      </div>
      <div
        style={{
          width: 1,
          flexShrink: 0,
          backgroundColor: 'var(--mantine-color-default-border)',
          position: 'relative',
        }}
      >
        <div
          {...splitter.getHandleProps({ index: 0 })}
          style={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: 20,
            height: 40,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            cursor: 'col-resize',
            touchAction: 'none',
            backgroundColor: 'var(--mantine-color-default)',
            border: '1px solid var(--mantine-color-default-border)',
            borderRadius: 'var(--mantine-radius-sm)',
            color: 'var(--mantine-color-dimmed)',
            zIndex: 1,
          }}
        >
          <DotsSixVerticalIcon />
        </div>
      </div>
      <div
        style={{
          width: `${splitter.sizes[1]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-teal-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel B ({Math.round(splitter.sizes[1] as number)}%)
      </div>
    </div>
  );
}
```


## Nested layout

Use several `useSplitter` instances to combine horizontal and vertical splits.
Here the right side of a horizontal split contains a vertical split:

```tsx
import { DotsSixVerticalIcon, DotsSixIcon } from '@phosphor-icons/react';
import { useSplitter } from '@mantine/hooks';

function Demo() {
  const horizontal = useSplitter({
    panels: [
      { defaultSize: 30, min: 15 },
      { defaultSize: 70, min: 30 },
    ],
  });

  const vertical = useSplitter({
    panels: [
      { defaultSize: 60, min: 20 },
      { defaultSize: 40, min: 20 },
    ],
    orientation: 'vertical',
  });

  return (
    <div
      ref={horizontal.ref}
      style={{
        display: 'flex',
        height: 350,
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          width: `${horizontal.sizes[0]}%`,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: 'var(--mantine-color-blue-filled)',
          color: 'var(--mantine-color-white)',
          fontWeight: 500,
          whiteSpace: 'nowrap',
          gap: 2,
        }}
      >
        Panel A ({Math.round(horizontal.sizes[0] as number)}%)
      </div>
      <div
        {...horizontal.getHandleProps({ index: 0 })}
        style={{
          width: 4,
          flexShrink: 0,
          cursor: 'col-resize',
          touchAction: 'none',
          backgroundColor: 'var(--mantine-color-default-border)',
          position: 'relative',
        }}
      >
        <div
          style={{
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: 8,
            height: 40,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: 'var(--mantine-radius-xs)',
            backgroundColor: 'var(--mantine-color-default)',
            border: '1px solid var(--mantine-color-default-border)',
            color: 'var(--mantine-color-dimmed)',
          }}
        >
          <DotsSixVerticalIcon style={{ width: '100%', height: '100%' }} />
        </div>
      </div>
      <div
        ref={vertical.ref}
        style={{
          width: `${horizontal.sizes[1]}%`,
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div
          style={{
            height: `${vertical.sizes[0]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-teal-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            gap: 2,
          }}
        >
          Panel B ({Math.round(vertical.sizes[0] as number)}%)
        </div>
        <div
          {...vertical.getHandleProps({ index: 0 })}
          style={{
            height: 4,
            flexShrink: 0,
            cursor: 'row-resize',
            touchAction: 'none',
            backgroundColor: 'var(--mantine-color-default-border)',
            position: 'relative',
          }}
        >
          <div
            style={{
              position: 'absolute',
              top: '50%',
              left: '50%',
              transform: 'translate(-50%, -50%)',
              width: 40,
              height: 8,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              borderRadius: 'var(--mantine-radius-xs)',
              backgroundColor: 'var(--mantine-color-default)',
              border: '1px solid var(--mantine-color-default-border)',
              color: 'var(--mantine-color-dimmed)',
            }}
          >
            <DotsSixIcon style={{ width: '100%', height: '100%' }} />
          </div>
        </div>
        <div
          style={{
            height: `${vertical.sizes[1]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-grape-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            gap: 2,
          }}
        >
          Panel C ({Math.round(vertical.sizes[1] as number)}%)
        </div>
      </div>
    </div>
  );
}
```


## Code editor layout

A collapsible file explorer sidebar, a code editor panel and a terminal:

```tsx
import { DotsSixVerticalIcon, DotsSixIcon } from '@phosphor-icons/react';
import { Code, ScrollArea, Text } from '@mantine/core';
import { useSplitter } from '@mantine/hooks';

const files = [
  'src/App.tsx',
  'src/index.tsx',
  'src/components/Header.tsx',
  'src/components/Sidebar.tsx',
  'src/hooks/useAuth.ts',
  'src/utils/api.ts',
  'package.json',
  'tsconfig.json',
];

const sampleCode = `import { useState } from 'react';
import { Header } from './components/Header';
import { Sidebar } from './components/Sidebar';

export function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="app">
      <Header title="My App" />
      <div className="content">
        <Sidebar />
        <main>
          <h1>Welcome</h1>
          <p>Count: {count}</p>
          <button onClick={() => setCount(c => c + 1)}>
            Increment
          </button>
        </main>
      </div>
    </div>
  );
}`;

const output = `[vite] Dev server running at:

  > Local:    http://localhost:5173/
  > Network:  http://192.168.1.42:5173/

  ready in 142ms.

[vite] page reload src/App.tsx
[vite] hmr update /src/App.tsx`;

const handleH: React.CSSProperties = {
  width: 4,
  flexShrink: 0,
  cursor: 'col-resize',
  touchAction: 'none',
  backgroundColor: 'var(--mantine-color-default-border)',
  position: 'relative',
};

const handleV: React.CSSProperties = {
  height: 4,
  flexShrink: 0,
  cursor: 'row-resize',
  touchAction: 'none',
  backgroundColor: 'var(--mantine-color-default-border)',
  position: 'relative',
};

const gripH: React.CSSProperties = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 8,
  height: 40,
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  borderRadius: 'var(--mantine-radius-xs)',
  backgroundColor: 'var(--mantine-color-default)',
  border: '1px solid var(--mantine-color-default-border)',
  color: 'var(--mantine-color-dimmed)',
};

const gripV: React.CSSProperties = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 40,
  height: 8,
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  borderRadius: 'var(--mantine-radius-xs)',
  backgroundColor: 'var(--mantine-color-default)',
  border: '1px solid var(--mantine-color-default-border)',
  color: 'var(--mantine-color-dimmed)',
};

function Demo() {
  const horizontal = useSplitter({
    panels: [
      { defaultSize: 20, min: 10, collapsible: true },
      { defaultSize: 80, min: 30 },
    ],
  });

  const vertical = useSplitter({
    panels: [
      { defaultSize: 70, min: 20 },
      { defaultSize: 30, min: 10 },
    ],
    orientation: 'vertical',
  });

  return (
    <div
      ref={horizontal.ref}
      style={{
        display: 'flex',
        height: 400,
        border: '1px solid var(--mantine-color-default-border)',
        borderRadius: 'var(--mantine-radius-md)',
        overflow: 'hidden',
        fontSize: 'var(--mantine-font-size-sm)',
      }}
    >
      <div style={{ width: \\`\\${horizontal.sizes[0]}%\\`, overflow: 'hidden' }}>
        <div
          style={{
            padding: '8px 12px',
            fontWeight: 600,
            borderBottom: '1px solid var(--mantine-color-default-border)',
            whiteSpace: 'nowrap',
          }}
        >
          Explorer
        </div>
        <ScrollArea h="calc(100% - 37px)">
          {files.map((file) => (
            <div
              key={file}
              style={{
                padding: '4px 12px',
                cursor: 'pointer',
                whiteSpace: 'nowrap',
                overflow: 'hidden',
                textOverflow: 'ellipsis',
              }}
            >
              {file}
            </div>
          ))}
        </ScrollArea>
      </div>

      <div {...horizontal.getHandleProps({ index: 0 })} style={handleH}>
        <div style={gripH}>
          <DotsSixVerticalIcon style={{ width: '100%', height: '100%' }} />
        </div>
      </div>

      <div
        ref={vertical.ref}
        style={{
          width: \\`\\${horizontal.sizes[1]}%\\`,
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div style={{ height: \\`\\${vertical.sizes[0]}%\\`, overflow: 'hidden' }}>
          <div
            style={{
              padding: '8px 12px',
              fontWeight: 600,
              borderBottom: '1px solid var(--mantine-color-default-border)',
              whiteSpace: 'nowrap',
            }}
          >
            src/App.tsx
          </div>
          <ScrollArea h="calc(100% - 37px)">
            <Code block style={{ border: 'none', borderRadius: 0, background: 'transparent' }}>
              {sampleCode}
            </Code>
          </ScrollArea>
        </div>

        <div {...vertical.getHandleProps({ index: 0 })} style={handleV}>
          <div style={gripV}>
            <DotsSixIcon style={{ width: '100%', height: '100%' }} />
          </div>
        </div>

        <div style={{ height: \\`\\${vertical.sizes[1]}%\\`, overflow: 'hidden' }}>
          <div
            style={{
              padding: '8px 12px',
              fontWeight: 600,
              borderBottom: '1px solid var(--mantine-color-default-border)',
              whiteSpace: 'nowrap',
            }}
          >
            Terminal
          </div>
          <ScrollArea h="calc(100% - 37px)">
            <Text
              component="pre"
              size="xs"
              style={{ margin: 0, padding: '8px 12px', fontFamily: 'monospace' }}
            >
              {output}
            </Text>
          </ScrollArea>
        </div>
      </div>
    </div>
  );
}
```


## Controlled mode

Pass `sizes` and `onSizeChange` to control the sizes externally.
Use `setSizes()` for programmatic updates:

```tsx
import { useState } from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { Button, Group, Text } from '@mantine/core';
import { useSplitter } from '@mantine/hooks';

function Demo() {
  const [sizes, setSizes] = useState([50, 50]);

  const splitter = useSplitter({
    panels: [
      { defaultSize: 50, min: 20 },
      { defaultSize: 50, min: 20 },
    ],
    sizes,
    onSizeChange: setSizes,
  });

  return (
    <>
      <div
        ref={splitter.ref}
        style={{
          display: 'flex',
          height: 200,
          borderRadius: 'var(--mantine-radius-md)',
          overflow: 'hidden',
        }}
      >
        <div
          style={{
            width: `${splitter.sizes[0]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-blue-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            gap: 2,
          }}
        >
          Panel A ({Math.round(splitter.sizes[0] as number)}%)
        </div>
        <div
          {...splitter.getHandleProps({ index: 0 })}
          style={{
            width: 4,
            flexShrink: 0,
            cursor: 'col-resize',
            touchAction: 'none',
            backgroundColor: 'var(--mantine-color-default-border)',
            position: 'relative',
          }}
        >
          <div
            style={{
              position: 'absolute',
              top: '50%',
              left: '50%',
              transform: 'translate(-50%, -50%)',
              width: 8,
              height: 40,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              borderRadius: 'var(--mantine-radius-xs)',
              backgroundColor: 'var(--mantine-color-default)',
              border: '1px solid var(--mantine-color-default-border)',
              color: 'var(--mantine-color-dimmed)',
            }}
          >
            <DotsSixVerticalIcon />
          </div>
        </div>
        <div
          style={{
            width: `${splitter.sizes[1]}%`,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-teal-filled)',
            color: 'var(--mantine-color-white)',
            fontWeight: 500,
            whiteSpace: 'nowrap',
            gap: 2,
          }}
        >
          Panel B ({Math.round(splitter.sizes[1] as number)}%)
        </div>
      </div>
      <Text size="sm" mt="sm">
        Current sizes: [{sizes.map((s) => Math.round(s)).join(', ')}]
      </Text>
      <Group mt="xs">
        <Button size="xs" onClick={() => setSizes([30, 70])}>30 / 70</Button>
        <Button size="xs" onClick={() => setSizes([50, 50])}>50 / 50</Button>
        <Button size="xs" onClick={() => setSizes([70, 30])}>70 / 30</Button>
      </Group>
    </>
  );
}
```


## Keyboard support

Handles follow the WAI-ARIA Window Splitter pattern:

* ArrowLeft/ArrowRight – resize by `step` (horizontal)
* ArrowUp/ArrowDown – resize by `step` (vertical)
* Shift + Arrow – resize by `shiftStep`
* Home – shrink the panel before the handle to its min
* End – grow the panel before the handle to its max
* Enter – toggle collapse of the smaller adjacent collapsible panel

## Touch support

The hook uses the Pointer Events API which handles both mouse and touch automatically.
Set `touch-action: none` on the handle element to prevent the browser from
interpreting touch drag as scroll:

```css
.handle {
  touch-action: none;
}
```

## Definition

```tsx
/** A bare number/`%` is a flexible size, `px`/`rem` is a fixed size */
type SplitterPaneSize = number | `${number}%` | `${number}px` | `${number}rem`;

/** A bare number/`%` is a percentage of the container, `px`/`rem` is resolved to pixels */
type SplitterStep = number | `${number}%` | `${number}px` | `${number}rem`;

interface UseSplitterPanel {
  /** Initial size, a `number`/`%` is flexible, `px`/`rem` is fixed. A bare number is a percentage. */
  defaultSize: SplitterPaneSize;
  /** Minimum size in the same units as `defaultSize`, `0` by default */
  min?: SplitterPaneSize;
  /** Maximum size in the same units as `defaultSize`, no limit by default */
  max?: SplitterPaneSize;
  /** Whether this panel can be collapsed, `false` by default */
  collapsible?: boolean;
  /** Size below which the panel snaps to collapsed, defaults to `min` */
  collapseThreshold?: SplitterPaneSize;
}

/** Panel config resolved to numeric units (percent or pixels) */
interface UseSplitterResolvedPanel {
  defaultSize: number;
  min?: number;
  max?: number;
  collapsible?: boolean;
  collapseThreshold?: number;
}

interface UseSplitterRedistributeInput {
  sizes: number[];
  panels: UseSplitterResolvedPanel[];
  handleIndex: number;
  delta: number;
}

type UseSplitterRedistributeFn = (input: UseSplitterRedistributeInput) => number[];

interface UseSplitterOptions {
  /** Panel configuration array (minimum 2 panels) */
  panels: UseSplitterPanel[];
  /** Layout direction, `'horizontal'` by default */
  orientation?: 'horizontal' | 'vertical';
  /** Controlled sizes, each value keeps the unit it was declared in */
  sizes?: SplitterPaneSize[];
  /** Called during resize with updated sizes, each value keeps its declared unit */
  onSizeChange?: (sizes: SplitterPaneSize[]) => void;
  /** Called when drag starts */
  onResizeStart?: (handleIndex: number) => void;
  /** Called when drag ends */
  onResizeEnd?: (handleIndex: number, sizes: SplitterPaneSize[]) => void;
  /** Called when a panel collapses or expands */
  onCollapseChange?: (panelIndex: number, collapsed: boolean) => void;
  /** How to borrow space from non-adjacent panels */
  redistribute?: 'nearest' | 'equal' | UseSplitterRedistributeFn;
  /** Keyboard step size, a `number`/`%` is a percentage, `px`/`rem` is pixels, `1` by default */
  step?: SplitterStep;
  /** Shift+arrow step size, a `number`/`%` is a percentage, `px`/`rem` is pixels, `10` by default */
  shiftStep?: SplitterStep;
  /** Text direction for keyboard nav, `'ltr'` by default */
  dir?: 'ltr' | 'rtl';
  /** Enable/disable the hook, `true` by default */
  enabled?: boolean;
}

interface UseSplitterReturnValue<T extends HTMLElement = any> {
  /** Ref callback for the container element */
  ref: React.RefCallback<T | null>;
  /** Current panel sizes, each value keeps the unit it was declared in */
  sizes: SplitterPaneSize[];
  /** Whether sizes are tracked in pixels because any pane size, `min`, `max`, `step`, `shiftStep`
   * or `collapseThreshold` uses a fixed `px`/`rem` unit */
  pixelMode: boolean;
  /** Which panels are currently collapsed */
  collapsed: boolean[];
  /** Index of handle being dragged, or -1 */
  activeHandle: number;
  /** Get props to spread on each resize handle */
  getHandleProps: (input: { index: number }) => HandleProps;
  /** Programmatically set sizes */
  setSizes: (sizes: SplitterPaneSize[]) => void;
  /** Collapse a panel */
  collapse: (panelIndex: number) => void;
  /** Expand a collapsed panel */
  expand: (panelIndex: number) => void;
  /** Toggle collapse of a panel */
  toggleCollapse: (panelIndex: number) => void;
}

function useSplitter<T extends HTMLElement = any>(
  options: UseSplitterOptions
): UseSplitterReturnValue<T>
```

## Exported types

`UseSplitterPanel`, `UseSplitterOptions`, `UseSplitterReturnValue`, `UseSplitterRedistributeInput`,
`UseSplitterRedistributeFn`, `UseSplitterResolvedPanel`, `SplitterPaneSize` and `SplitterStep` types
are exported from the `@mantine/hooks` package:

```tsx
import type {
  SplitterPaneSize,
  SplitterStep,
  UseSplitterPanel,
  UseSplitterOptions,
  UseSplitterReturnValue,
  UseSplitterRedistributeInput,
  UseSplitterRedistributeFn,
  UseSplitterResolvedPanel,
} from '@mantine/hooks';
```
