← Back toHTML DOM

Create resizable split views

Written byPhuoc Nguyen
Created
04 Apr, 2020
Category
Level 3 — Advanced
In this post, we'll add an element to resize children of a given element. The original element could be organized as below:
html
<div style="display: flex">
<!-- Left element -->
<div>Left</div>

<!-- The resizer -->
<div class="resizer" id="dragMe"></div>

<!-- Right element -->
<div>Right</div>
</div>
In order to place the left, resizer and right elements in the same row, we add the `display: flex` style to the parent.

Update the width of left side when dragging the resizer element

It's recommended to look at this post to see how we can make an element draggable.
In our case, the resizer can be dragged horizontally. First, we have to store the mouse position and the left side's width when user starts clicking the resizer:
js
// Query the element
const resizer = document.getElementById('dragMe');
const leftSide = resizer.previousElementSibling;
const rightSide = resizer.nextElementSibling;

// The current position of mouse
let x = 0;
let y = 0;

// Width of left side
let leftWidth = 0;

// Handle the mousedown event
// that's triggered when user drags the resizer
const mouseDownHandler = function (e) {
// Get the current mouse position
x = e.clientX;
y = e.clientY;
leftWidth = leftSide.getBoundingClientRect().width;

// Attach the listeners to `document`
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};

// Attach the handler
resizer.addEventListener('mousedown', mouseDownHandler);
Looking at the structure of our markup, the left and right side are previous and next sibling of resizer. They can be retrieved as you see above:
js
const leftSide = resizer.previousElementSibling;
const rightSide = resizer.nextElementSibling;
Next, when user moves the mouse around, we determine how far the mouse has been moved and then update the width for the left side:
js
const mouseMoveHandler = function (e) {
// How far the mouse has been moved
const dx = e.clientX - x;
const dy = e.clientY - y;

const newLeftWidth = ((leftWidth + dx) * 100) / resizer.parentNode.getBoundingClientRect().width;
leftSide.style.width = `${newLeftWidth}%`;
};
There're two important things that I would like to point out here:
  • The width of left side is set based on the number of percentages of the parent's width. It keeps the ratio of left and side widths, and makes two sides look good when user resizes the browser.
  • It's not necessary to update the width of right side if we always force it to take the remaining width:
html
<div style="display: flex">
<!-- Left element -->
...

<!-- The resizer -->
...

<!-- Right element -->
<div style="flex: 1 1 0%;">Right</div>
</div>

Fix the flickering issue

When user moves the resizer, we should update its cursor:
js
const mouseMoveHandler = function(e) {
...
resizer.style.cursor = 'col-resize';
};
But it causes another issue. As soon as the user moves the mouse around, we will see the default mouse cursor beause the mouse isn't on top of the resizer. User will see the screen flickering because the cursor is changed continuously.
To fix that, we set the cursor for the entire page:
js
const mouseMoveHandler = function(e) {
...
document.body.style.cursor = 'col-resize';
};
We also prevent the mouse events and text selection in both sides by setting the values for `user-select` and `pointer-events`:
js
const mouseMoveHandler = function(e) {
...
leftSide.style.userSelect = 'none';
leftSide.style.pointerEvents = 'none';

rightSide.style.userSelect = 'none';
rightSide.style.pointerEvents = 'none';
};
These styles are removed right after the user stops moving the mouse:
js
const mouseUpHandler = function () {
resizer.style.removeProperty('cursor');
document.body.style.removeProperty('cursor');

leftSide.style.removeProperty('user-select');
leftSide.style.removeProperty('pointer-events');

rightSide.style.removeProperty('user-select');
rightSide.style.removeProperty('pointer-events');

// Remove the handlers of `mousemove` and `mouseup`
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
Below is the demo that you can play with.

Support vertical direction

It's easy to support splitting the side vertically. Instead of updating the width of left side, now we update the height of the top side:
js
const prevSibling = resizer.previousElementSibling;
let prevSiblingHeight = 0;

const mouseDownHandler = function (e) {
const rect = prevSibling.getBoundingClientRect();
prevSiblingHeight = rect.height;
};

const mouseMoveHandler = function (e) {
const h = ((prevSiblingHeight + dy) * 100) / resizer.parentNode.getBoundingClientRect().height;
prevSibling.style.height = `${h}%`;
};
We also change the cursor when user moves the resizer element:
js
const mouseMoveHandler = function(e) {
...
resizer.style.cursor = 'row-resize';
document.body.style.cursor = 'row-resize';
};

Support both directions

Let's say that the right side wants to be split into two resizable elements.
We have two resizer elements currently. To indicate the splitting direction for each resizer, we add a custom attribute `data-direction`:
html
<div style="display: flex">
<div>Left</div>
<div class="resizer" data-direction="horizontal"></div>

<!-- The right side -->
<div style="display: flex; flex: 1 1 0%; flex-direction: column">
<div>Top</div>
<div class="resizer" data-direction="vertical"></div>
<div style="flex: 1 1 0%">Bottom</div>
</div>
</div>
Later, we can retrieve the attribute from the resizer element:
js
const direction = resizer.getAttribute('data-direction') || 'horizontal';
The logic of setting the width or height of previous sibling depends on the direction:
js
const mouseMoveHandler = function(e) {
switch (direction) {
case 'vertical':
const h = (prevSiblingHeight + dy) * 100 / resizer.parentNode.getBoundingClientRect().height;
prevSibling.style.height = `${h}%`;
break;
case 'horizontal':
default:
const w = (prevSiblingWidth + dx) * 100 / resizer.parentNode.getBoundingClientRect().width;
prevSibling.style.width = `${w}%`;
break;
}

const cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
resizer.style.cursor = cursor;
document.body.style.cursor = cursor;

...
};
Tip
Tip
Using custom `data-` attribute is a good way to manage variables associated with the element
Enjoy the demo!

Demo

See also

If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn