Creating Resizable Split Panes From Scratch
Published: 2024-01-13 13:03:06

by [Ayomipo Odeyinde](https://blog.openreplay.com/authors/ayomipo-odeyinde)


<blockquote><em>
Resizable split panes work by splitting a window into separate sections, each showing different content. Each section is called a pane. And here's the best part: these panes can be resized on the fly by dragging a little handle between them, and they'll resize accordingly. This flexibility is useful for platforms where users can simultaneously view and work on multiple things. In this tutorial, you'll use React and TailwindCSS to handle the resizing behavior, styling, and structure of these resizable panes.
</em></blockquote>


<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>


# How to Create Resizable Split Panes from Scratch using React and TailwindCSS

You've probably seen the resizable split panes feature before on some platforms.

![1](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image1.gif)

Let's begin by ensuring all the necessary tools are installed.

First, ensure that you have [Node.js](https://nodejs.org/en/download) installed on your computer.

### Creating a New React Project

`create-react-app` would be used as it is the most popular and straightforward option.

Run these commands in your terminal:

```bash
npx create-react-app resizable-panes-app
cd resizable-panes-app
```

### Linking TailwindCSS

For styling, TailwindCSS would be used. Run these commands to install and setup TailwindCSS:

```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
```

Then add the Tailwind directives to the `index.css` file:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

And with everything fully set, run the build process for the react app:

```bash
npm run start
```

## Implementing Basic Split Panes

Let's implement three functional components: the default, `App.js`, and two others, `ResizablePanes` and `ResizablePane`.

```javascript
export default function App() {
  return <ResizablePanes />;
}

function ResizablePanes() {
  return (
    <div className="w-screen h-screen flex">
      <ResizablePane bgColor={"bg-red-400"} />
      <ResizablePane bgColor={"bg-yellow-400"} />
    </div>
  );
}

function ResizablePane({ bgColor }) {
  return <div className={`${bgColor}`} style={{ width: "200px" }}></div>;
}
```

The `ResizablePanes` component would act as the container for all the different `ResizablePane` components that would be created. The `ResizablePane` component would act as each pane's basic structure and appearance.

You should have a layout like this:

![2](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image2.png)

Now, the logic behind resizing a pane is simple. It all comes down to tracking the direction and distance of the mouse movement within a pane.

Luckily, JavaScript provides the right mouse event properties for this — the `movementX` and `movementY` properties. Note: You may think why not use `clientX` and `clientY` instead. Yes, that'd also work in providing the absolute mouse location. However, `movementX` and `movementY` determine how much the mouse moved in each direction, which is much easier and more accurate.

With this movement data, it's all about adding or subtracting it from the pane's current size based on the direction you're dragging. This way, the pane resizes smoothly and dynamically in real time.

Now, back to the code.

Let's start by defining each pane's initial size (width) and a way to track it dynamically. We'll use the `useState` hook for this:

```javascript
function ResizablePane({ initialSize, bgColor }) {
  const [size, setSize] = useState(initialSize);

  return <div className={`${bgColor}`} style={{ width: `${size}px` }}></div>;
}
```

Next, in the `ResizablePanes` component, we update the `ResizablePane` components rendered with the desired `initialSize`.

```javascript
function ResizablePanes() {
  return (
    <div className="w-screen h-screen flex">
      <ResizablePane initialSize={200} bgColor={"bg-red-400"} />
      <ResizablePane initialSize={200} bgColor={"bg-yellow-400"} />
    </div>
  );
}
```

And with this, we have laid the foundation for dynamic resizing.

Next, let's apply the mouse movement logic.

In the `ResizablePane` component, a `useEffect` hook would be used in setting up (and cleaning up) the event listeners for mouse movements.

```javascript
function ResizablePane({ initialSize, bgColor }) {
  const [size, setSize] = useState(initialSize);

  useEffect(() => {
    const handleMouseMove = (e) => {
      const movement = e.movementX;
      setSize(size + movement);
    };

    document.addEventListener("mousemove", handleMouseMove);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
    };
  }, [size]);

  //...
}
```

The `handleMouseMove` function in the `useEffect` hook first captures the `movementX` property of the mouse event object, which represents the horizontal movement of the mouse.

This is then added to the current `size` value, updated through `setSize()`.

Then, the `handleMouseMove` function is called whenever the mouse moves anywhere on the document by listening to the `mousemove` event. And we clean up the event listener to avoid memory leaks.

With this code in place, each `ResizablePane` now responds to mouse movements and dynamically adjusts its width.

![3](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image3.gif)

There's a slight issue — both panes are being resized simultaneously. This shouldn't be the case, as we should be able to choose which pane to resize.

To fix this, we need a way to identify the specific pane under the mouse's control.

```javascript
function ResizablePane({ initialSize, bgColor }) {
  const [size, setSize] = useState(initialSize);
  const [isResizing, setIsResizing] = useState(false);
  // ...
}
```

A new state, `isResizing`, is used, which is initially set to `false` and would only be `true` when you click and hold down on a pane.

Now let's modify the `handleMouseMove` function to check if the `isResizing` state is `true` before applying the resizing logic. If `false`, the pane will not be resized.

```javascript
const handleMouseMove = (e) => {
  if (!isResizing) return;

  const movement = e.movementX;
  setSize(size + movement);
};
```

To allow only the pane clicked to use the resizing logic, let's create a `handleMouseDown` function that sets `isResizing` to `true`.

```javascript
const handleMouseDown = () => setIsResizing(true);
```

Then add an `onMouseDown` event listener to the `ResizablePane`, which invokes the `handleMouseDown` function.

```javascript
function ResizablePane({ initialSize, bgColor }) {
  //...

  return (
    <div
      className={`${bgColor}`}
      style={{ width: `${size}px` }}
      onMouseDown={handleMouseDown}
    ></div>
  );
}
```

With this, only the pane you click and hold on will respond to mouse movements and will be resized as needed.

![4](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image4.gif)

Right now, the pane keeps resizing even if you let go of the mouse button. We need a way to fix that.

In the `useEffect` hook in `ResizablePane`, create a function, `handleMouseUp` that set `isResizing` to `false`. Then add a `mouseup` event listener to the document, which triggers `handleMouseUp` when the mouse button is released.

```javascript
useEffect(() => {
  const handleMouseMove = (e) => {
    // ...
  };
  const handleMouseUp = () => setIsResizing(false);

  document.addEventListener("mousemove", handleMouseMove);
  document.addEventListener("mouseup", handleMouseUp);
  return () => {
    document.removeEventListener("mousemove", handleMouseMove);
    document.removeEventListener("mouseup", handleMouseUp);
  };
}, [size, isResizing]);
```

With these changes, the resizing stops as soon as you let go, and it should all work as expected now.

Also, just for fun, let's add a third `ResizablePane` to our layout.

```javascript
function ResizablePanes() {
  return (
    <div className="w-screen h-screen flex">
      <ResizablePane initialSize={200} bgColor={"bg-red-400"} />
      <ResizablePane initialSize={300} bgColor={"bg-yellow-400"} />
      <ResizablePane initialSize={150} bgColor={"bg-emerald-400"} />
    </div>
  );
}
```

![5](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image5.gif)

### Stretching Panes to Fit the Screen

Typically, split panes cover the entire screen, with at least one pane expanding to fill any remaining space.

To achieve this, `flex-grow` can be used.

```javascript
function ResizablePane({ initialSize, grow, bgColor }) {
  //...
  return (
    <div
      className={`${bgColor} ${grow ? "grow" : ""} shrink-0`}
      style={{ width: `${size}px` }}
      onMouseDown={handleMouseDown}
    ></div>
  );
}
```

`ResizablePane` takes another prop, `grow`, which defaults to `false.` Thus, depending on the `grow` prop boolean value, the TailwindCSS `grow` class (equivalent to `flex-grow`) would be added to the `ResizablePane`.

When set to `true`, this prop tells the pane to automatically grow and fill any remaining space on the screen.

Let's update the `ResizablePanes` component to utilize the `grow` prop:

```javascript
function ResizablePanes() {
  return (
    <div className="w-screen h-screen flex">
      <ResizablePane initialSize={200} bgColor={"bg-red-400"} />
      <ResizablePane initialSize={300} grow="true" bgColor={"bg-yellow-400"} />
      <ResizablePane initialSize={150} bgColor={"bg-emerald-400"} />
    </div>
  );
}
```

Now, the split panes fill up the screen nicely.

![6](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image6.gif)

### Adding Resizing Handles

Let's give the resizable panes a handle that would act as the only clickable area that triggers the resizing functionality.

Create a new component called `ResizableHandle`:

```javascript
function ResizableHandle({ isResizing, handleMouseDown }) {
  return (
    <div
      className={`absolute w-1 top-0 bottom-0 right-0 cursor-col-resize hover:bg-blue-600 ${
        isResizing ? "bg-blue-600" : ""
      }`}
      onMouseDown={handleMouseDown}
    />
  );
}
```

The `ResizableHandle` is styled as a really thin vertical element that's positioned on the right side of the pane. However, you can style it any way you see fit.

The `ResizableHandle` uses the `isResizing` value to highlight the handle when it is being resized; this provides visual feedback.

Previously, the `onMouseDown` event was directly attached to the `ResizablePane`. However, now that responsibility has been passed down to the `ResizableHandle`.

Then render the `ResizableHandle` inside the `ResizablePane` and pass the necessary props.

```javascript
function ResizablePane({ initialSize, grow, bgColor }) {
  //...
  return (
    <div
      className={`relative ${bgColor} ${grow ? "grow" : ""} shrink-0`}
      style={{ width: `${size}px` }}
    >
      <ResizableHandle
        isResizing={isResizing}
        handleMouseDown={handleMouseDown}
      />
    </div>
  );
}
```

In most cases, the pane selected to grow shouldn't be resizable. This is because it could create empty spaces if resized lower than the available screen, making the layout unrealistic.

Let's disable resizing for the expanding pane:

```javascript
function ResizablePane({ initialSize, grow, bgColor }) {
  //...
  return (
    <div
      className={`relative ${bgColor} ${grow ? "grow" : ""} shrink-0`}
      style={{ width: `${size}px` }}
    >
      {!grow && (
        <ResizableHandle
          isResizing={isResizing}
          handleMouseDown={handleMouseDown}
        />
      )}
    </div>
  );
}
```

Now, if a `ResizablePane` has a `grow` prop set to `true`, it won't render a `ResizableHandle`, disabling resizing for that pane.

![7](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image7.gif)


<CTA_Middle_Design />



## Advanced Resizable Split Panes

Now that we've built the core resizing functionality of the split panes let's go a little bit advanced. In this section, we would:

* Include minimum and maximum constraints
* Set up vertical and horizontal split panes
    

### Minimum and Maximum Constraints

The split panes can't shrink below or grow beyond a certain size in real-world applications. This is mainly done to prevent users from resizing the panes to unusable sizes.

In the `ResizablePane` component, introduce two new props, `minSize` and `maxSize`:

```javascript
function ResizablePane({ minSize, initialSize, maxSize, grow, bgColor }) {
  //...
}
```

Then modify the `handleMouseMove` function located in the `ResizablePane` component:

```javascript
const handleMouseMove = (e) => {
  if (!isResizing) return;

  const movement = e.movementX;
  let newSize = size + movement;

  newSize = Math.max(minSize, Math.min(newSize, maxSize));
  setSize(newSize);
};
```

In this code, `Math.max()` ensures the new size doesn't fall below the `minSize`, and `Math.min` prevents it from exceeding the `maxSize`.

Here is how it works, `Math.min(newSize, maxSize)` would return `maxSize` only if `newSize` is greater, else it'd return `newSize`.

Then the derived value from `Math.min()` is passed to the `Math.max()` along with `minSize`, which would return `minSize` only if the derived value is less than `minSize`, else it'd return the derived value from `Math.min()`.

Finally, in `ResizablePanes`, let's set the desired minimum and maximum sizes for each pane:

```javascript
function ResizablePanes() {
  return (
    <div className="w-screen h-screen flex">
      <ResizablePane
        minSize={150}
        initialSize={200}
        maxSize={300}
        bgColor={"bg-red-400"}
      />
      <ResizablePane
        minSize={150}
        initialSize={300}
        grow="true"
        bgColor={"bg-yellow-400"}
      />
      <ResizablePane
        minSize={150}
        initialSize={150}
        maxSize={300}
        bgColor={"bg-emerald-400"}
      />
    </div>
  );
}
```

If you notice, there isn't a `maxSize` prop for the second `ResizablePane`. It is not needed because its `flex-grow` property would always expand to fill any available space and override any width set for that element.

![8](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image8.gif)

As a plus, you can even use the `ResizablePane` on the left-hand side as a dedicated sidebar, and it'd work perfectly. Just set a minimum and maximum constraint on the `ResizablePane` of choice, and it won't interfere with the main content area.

### Vertical and Horizontal Split Panes

So far, the resizable split panes have been built to only resize horizontally. However, you might need split panes to be vertically aligned and resized.

Doing this is quite simple, but first, recollect that the `ResizablePanes` flex layout direction is set to a row by default. Thus, to achieve vertical stacking, let's switch to a column layout.

Head over to the `ResizablePanes` component:

```javascript
function ResizablePanes({ direction }) {
  const isVertical = direction === "vertical";

  return (
    <div
      className={`w-screen h-screen flex ${
        isVertical ? "flex-col" : "flex-row"
      }`}
    >
      {/* Resizable pane components... */}
    </div>
  );
}
```

A new prop, `direction`, and a variable, `isVertical` is created that check if the desired direction is set to "vertical".

In the styling of the `ResizablePane` element, we dynamically add the `flex-col` or `flex-row` class depending on the `isVertical` value.

Moving on, in the `App` component, set the rendered `ResizablePane` `direction` prop to "vertical":

```javascript
export default function App() {
  return <ResizablePanes direction='vertical' />;
}
```

Next, the `isVertical` value is passed as a prop to `ResizablePane` for further use.

```javascript
function ResizablePane({
  minSize,
  initialSize,
  maxSize,
  grow,
  isVertical,
  bgColor,
}) {
  //...
}
```

Note: Also, don't forget to update the `ResizablePane` component when it is being rendered with prop, `isVertical`.

Next, let's modify the `handleMouseMove` function to use the appropriate mouse event property.

```javascript
const handleMouseMove = (e) => {
  if (!isResizing) return;

  const movement = isVertical ? e.movementY : e.movementX;
  let newSize = size + movement;

  newSize = Math.max(minSize, Math.min(maxSize, newSize));
  setSize(newSize);
};
```

This code checks the `isVertical` value and if it's true, it uses `e.movementY` to capture the vertical movement for resizing. Otherwise, it sticks with `e.movementX` for horizontal resizing.

Furthermore, since we're dealing with a vertical layout, the dimension that should be resized is the pane's height.

In `ResizablePane`, create a `dimension` variable which would store whether to resize the pane's `width` or `height` depending on the `isVertical` value.

```javascript
const dimension = isVertical ? "height" : "width";
```

Then we just dynamically update the CSS `style` property name, which could either be `width` or `height` depending on `dimension`.

```javascript
function ResizablePane({
  minSize,
  initialSize,
  maxSize,
  grow,
  isVertical,
  bgColor,
}) {
  // ...
  return (
    <div
      className={`relative ${bgColor} ${grow ? "grow" : ""} shrink-0`}
      style={{ [dimension]: `${size}px` }}
    >
      {/* ... */}
    </div>
  );
}
```

It'd work a little bit well now, but the `ResizableHandle` is still positioned for horizontal resizing. Let's adjust that by heading over to `ResizableHandle`:

```javascript
function ResizableHandle({ isResizing, isVertical, handleMouseDown }) {
  const positionHandleStyle = isVertical
    ? "h-1 left-0 right-0 bottom-0 cursor-row-resize"
    : "w-1 top-0 bottom-0 right-0 cursor-col-resize";

  return (
    <div
      className={`absolute ${positionHandleStyle} hover:bg-blue-600 ${
        isResizing ? "bg-blue-600" : ""
      }`}
      onMouseDown={handleMouseDown}
    />
  );
}
```

`isVertical` is passed as a prop to `ResizableHandle`; it is then used to switch the handle location to either the right or bottom depending on whether `isVertical` is true or not.

Finally, update the rendered `ResizableHandle` with the `isVertical` prop:

```javascript
<ResizableHandle
  isResizing={isResizing}
  isVertical={isVertical}
  handleMouseDown={handleMouseDown}
/>
```

It should work as expected now, and you can opt for either vertical or horizontal panes using the same code.

![9](https://blog.openreplay.com/images/resizable-split-panes-from-scratch/images/image9.gif)

## Alternative libraries for React Split Panes

Let's face it: sometimes we all just want a shortcut.

So here are some great resizable split pane libraries for React:

* [react-resizable-panels](https://github.com/bvaughn/react-resizable-panels)
* [react-splitter-layout](https://github.com/zesik/react-splitter-layout)
* [split-pane-react](https://github.com/yyllff/split-pane-react)
* [react-split-pane](https://github.com/tomkp/react-split-pane)

These are just a few of the amazing libraries available. If you need something basic, go for [react-resizable-panels](https://github.com/bvaughn/react-resizable-panels). And if you need more advanced features, you should look into [react-splitter-layout](https://github.com/zesik/react-splitter-layout) or [react-split-pane](https://github.com/tomkp/react-split-pane).

## Conclusion

So, there it is — a basic build of resizable split panes from scratch. You can find the [complete code here](https://github.com/Vayyo/resizable-split-panes).

Remember, building from scratch can be a rewarding experience, but it's not always the most efficient. Don't hesitate to use libraries to save yourself time and frustration.