Implementing Resizable Elements with Vue Directives (v‑resize)
This article explains how to create a Vue directive for element resizing, covering basic right‑side resizing, handling trigger zones, enforcing minimum dimensions, adding left/right multi‑direction support, and providing complete JavaScript and TypeScript implementations with usage examples and future enhancements.
Introduction
In many projects we need to stretch layout elements; this guide shows how to achieve that using a custom Vue directive.
Online Demo
Try the live example at Codesandbox .
Basic Right‑Side Resize
We start with a simple right‑resize using the v-resize.right modifier.
const pointermove = e => {
const { right } = el.getBoundingClientRect();
const { clientX } = e;
if (right - clientX < 8) {
// can stretch
}
};Template for two adjacent divs:
<template>
<div class="container">
<div v-resize.right class="left"/>
<div class="right"/>
</div>
</template>
<style lang="scss" scoped>
.container {
width: 400px;
height: 100px;
> div { float: left; height: 100%; width: 50%; }
.left { background-color: lightcoral; }
.right { background-color: lightblue; }
}
</style>The directive detects the trigger zone (8 px from the right edge) and changes the cursor to col-resize :
export const resize = {
inserted: function(el, binding) {
el.addEventListener('pointermove', (e) => {
const { right } = el.getBoundingClientRect();
if (right - e.clientX < 8) {
el.style.cursor = 'col-resize';
} else {
el.style.cursor = '';
}
});
}
};We also handle pointerdown to start resizing, calculate offsets, and update the widths of the target and its sibling while enforcing a minimum width:
el.addEventListener('pointerdown', (e) => {
const rightDom = el.nextElementSibling;
const startX = e.clientX;
const { width } = el.getBoundingClientRect();
const { width: nextWidth } = rightDom.getBoundingClientRect();
el.setPointerCapture(e.pointerId);
const onDocumentMouseMove = (e) => {
const offsetX = e.clientX - startX;
if (width + offsetX < MIN_WIDTH || nextWidth - offsetX < MIN_WIDTH) return;
el.style.width = width + offsetX + 'px';
rightDom.style.width = nextWidth - offsetX + 'px';
};
document.addEventListener('mousemove', onDocumentMouseMove);
el.addEventListener('pointerup', (e) => {
el.releasePointerCapture(e.pointerId);
document.removeEventListener('mousemove', onDocumentMouseMove);
});
});Two issues were observed: the left width could exceed the total and the resize continued after mouse release. They are solved by limiting minimum width and releasing the pointer on pointerup .
Advanced Left/Right Multi‑Direction Resize
For simultaneous left and right resizing we track the current position (left or right) and a resizing flag.
export const resize = {
inserted: function (el, binding) {
let position = '', resizing = false;
el.addEventListener('pointermove', (e) => {
if (resizing) return;
const { left, right } = el.getBoundingClientRect();
const { clientX } = e;
if (right - clientX < 8) { position = 'right'; el.style.cursor = 'col-resize'; }
else if (clientX - left < 8) { position = 'left'; el.style.cursor = 'col-resize'; }
else { position = ''; el.style.cursor = '' }
});
// pointerdown, mousemove, pointerup logic similar to the right‑only version
// but uses the detected position to adjust widths of the element and its sibling accordingly
}
};The full JavaScript implementation includes event binding, cleanup via a WeakMap , support for minimum dimensions, and optional flex‑based sizing.
Complete JavaScript Version
const elEventsWeakMap = new WeakMap();
const MIN_WIDTH = 50;
const MIN_HEIGHT = 50;
const TRIGGER_SIZE = 8;
// utility functions: getElStyleAttr, getSiblingByPosition, getSiblingsSize, updateSize
// initResize sets up state, default cursor, and event listeners (pointermove, pointerdown, pointerup, pointerleave)
// bindElEvents / unBindElEvents manage lifecycle
export const resize = {
inserted: function(el, binding) {
const { modifiers, value } = binding;
const positions = Object.keys(modifiers);
initResize({ el, positions, ...value });
},
unbind: function(el) {
const unBindElEvents = elEventsWeakMap.get(el);
unBindElEvents();
}
};TypeScript Version
import type { DirectiveBinding } from 'vue';
const elEventsWeakMap = new WeakMap();
const MIN_WIDTH = 50;
const MIN_HEIGHT = 50;
const TRIGGER_SIZE = 8;
enum RESIZE_CURSOR { COL_RESIZE = 'col-resize', ROW_RESIZE = 'row-resize' }
enum POSITION { TOP = 'top', BOTTOM = 'bottom', LEFT = 'left', RIGHT = 'right' }
// Types: Positions, ResizeState, ResizeInfo, etc.
// Functions: getElStyleAttr, getSiblingByPosition, getSiblingsSize, updateSize, initResize
export const resize = {
mounted: function (el: HTMLElement, binding: DirectiveBinding) {
const { modifiers, value } = binding;
const positions = Object.keys(modifiers);
initResize({ el, positions, ...value });
},
beforeUnmount: function (el: HTMLElement) {
const unBindElEvents = elEventsWeakMap.get(el);
unBindElEvents();
}
};Future Work
The author plans to create a v-ellipsis-tooltip directive based on el-tooltip to show tooltips only when text overflows.
References: Juejin article 1 , Juejin article 2 .
End note: the author welcomes feedback and encourages readers to like, follow, and bookmark the article.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.