Frontend Development 15 min read

Implementing a Draggable and Resizable Container Component in Vue

This article explains how to create a Vue component that supports both dragging and resizing by rendering CSS edges and corners, handling mouse events to track movement, calculating new dimensions, enforcing minimum sizes, and providing a complete template, script, and style implementation.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing a Draggable and Resizable Container Component in Vue

When a UI requires drag‑and‑resize functionality, the core idea is to place invisible handles on the four sides and four corners of a container and update its size and position based on mouse movements.

Feature Demonstration

Images illustrate the scaling and moving effects; a demo link is provided.

Design Thinking for Scaling

Use CSS to draw four borders and four corner handles.

Position the lines and corners with CSS positioning.

Listen for mouse down and move events.

During movement, adjust the container's width, height, and position.

Core Design

Basic HTML Structure

<template>
<!-- Insert into body conditionally -->
<div ref="draggableContainer" class="draggable-container" @mousedown="startDrag" :style="containerStyle">
<slot></slot>
<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>
</div>
</template>

Basic Data

data: {
resizeTypes: ["lt", "t", "rt", "r", "rb", "b", "lb", "l"],
position: { x: this.left, y: this.top },
size: { width: this.width, height: this.height },
originMouseX: 0, originMouseY: 0,
originContainX: 0, originContainY: 0,
originWidth: 0, originHeight: 0,
resizeType: ""
};

Core Code and Logic

Container new width = initial width + mouse movement distance

The algorithm records the container's initial size and mouse position, computes the delta, and updates width, height, and position according to the dragged edge.

1. Record Initial States

// Mouse down, start resize
startResize(event) {
this.originMouseX = event.clientX;
this.originMouseY = event.clientY;
this.originWidth = this.size.width;
this.originHeight = this.size.height;
this.originContainX = this.position.x;
this.originContainY = this.position.y;
},

2. Compute New Width for Right Edge

// deltaX = mouse move distance
const deltaX = event.clientX - this.originMouseX;
newWidth = this.originWidth + deltaX;

3. Determine Which Edge Is Dragged

<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>

4. Enforce Minimum Width/Height

if (newWidth >= this.minWidth) { this.size.width = newWidth; }
if (newHeight >= this.minHeight) { this.size.height = newHeight; }

5. Add Drag‑Move Logic

The full component combines drag‑move and resize logic.

Complete Code

<template>
<div ref="draggableContainer" class="draggable-container" @mousedown="startDrag" :style="containerStyle">
<slot></slot>
<span v-for="type in resizeTypes" :key="type" :class="`${type}-resize`" @mousedown="startResize($event, type)"></span>
</div>
</template>
<script>
export default {
props: { zIndex: {type: Number, default: 1}, left:{type:Number,default:0}, top:{type:Number,default:0}, width:{type:Number,default:300}, height:{type:Number,default:300}, minWidth:{type:Number,default:100}, minHeight:{type:Number,default:100} },
data() { return { resizeTypes:["lt","t","rt","r","rb","b","lb","l"], position:{x:this.left,y:this.top}, size:{width:this.width,height:this.height}, originMouseX:0, originMouseY:0, originContainX:0, originContainY:0, originWidth:0, originHeight:0, resizeType:"" }; },
computed:{ containerStyle(){ return { top:`${this.position.y}px`, left:`${this.position.x}px`, width:`${this.size.width}px`, height:`${this.size.height}px`, zIndex:this.zIndex }; } },
methods:{
startDrag(event){ this.originMouseX=event.clientX; this.originMouseY=event.clientY; this.originContainX=this.position.x; this.originContainY=this.position.y; document.addEventListener("mousemove",this.handleDrag); document.addEventListener("mouseup",this.stopDrag); },
handleDrag(event){ this.position.x = this.originContainX + event.clientX - this.originMouseX; this.position.y = this.originContainY + event.clientY - this.originMouseY; },
startResize(event,type){ this.resizeType=type; this.originMouseX=event.clientX; this.originMouseY=event.clientY; this.originWidth=this.size.width; this.originHeight=this.size.height; this.originContainX=this.position.x; this.originContainY=this.position.y; event.stopPropagation(); document.addEventListener("mousemove",this.handleResize); document.addEventListener("mouseup",this.stopDrag); },
handleResize(event){ const deltaX=event.clientX-this.originMouseX; const deltaY=event.clientY-this.originMouseY; let newWidth=this.originWidth; let newHeight=this.originHeight; switch(this.resizeType){ case "lt": newWidth=this.originWidth-deltaX; newHeight=this.originHeight-deltaY; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "t": newHeight=this.originHeight-deltaY; if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "rt": newWidth=this.originWidth+deltaX; newHeight=this.originHeight-deltaY; if(newWidth>=this.minWidth){ this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.position.y=this.originContainY+deltaY; this.size.height=newHeight; } break; case "r": newWidth=this.originWidth+deltaX; if(newWidth>=this.minWidth){ this.size.width=newWidth; } break; case "rb": newWidth=this.originWidth+deltaX; newHeight=this.originHeight+deltaY; if(newWidth>=this.minWidth){ this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "b": newHeight=this.originHeight+deltaY; if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "lb": newWidth=this.originWidth-deltaX; newHeight=this.originHeight+deltaY; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } if(newHeight>=this.minHeight){ this.size.height=newHeight; } break; case "l": newWidth=this.originWidth-deltaX; if(newWidth>=this.minWidth){ this.position.x=this.originContainX+deltaX; this.size.width=newWidth; } break; } },
stopDrag(){ document.removeEventListener("mousemove",this.handleDrag); document.removeEventListener("mousemove",this.handleResize); document.removeEventListener("mouseup",this.stopDrag); }
},
beforeDestroy(){ this.stopDrag(); }
};
</script>
<style lang="scss" scoped>
.draggable-container{ position:fixed; cursor:move; user-select:none; background:#ccc; span{position:absolute;display:block;} .l-resize,.r-resize{width:8px;height:100%;top:0;cursor:w-resize;} .l-resize{left:-6px;} .r-resize{right:-6px;} .t-resize,.b-resize{width:100%;height:8px;left:0;cursor:s-resize;} .t-resize{top:-6px;} .b-resize{bottom:-6px;} .lt-resize,.rt-resize,.rb-resize,.lb-resize{width:15px;height:15px;z-index:10;} .lt-resize,.lb-resize{left:-8px;} .lt-resize,.rt-resize{top:-8px;} .rt-resize,.rb-resize{right:-8px;} .rb-resize,.lb-resize{bottom:-8px;} .lt-resize,.rb-resize{cursor:se-resize;} .rt-resize,.lb-resize{cursor:sw-resize;} }
</style>

Component Usage

<DraggableContainer :width="400" :height="400" :min-height="300" :min-width="300">
<div>能拖动我了</div>
</DraggableContainer>

Conclusion

Part of the design references the popular third‑party library vxe‑modal.

The article provides a complete, functional draggable‑and‑resizable component that can be extended with custom features as needed.

frontendJavaScriptcomponentVueCSSdraggableresizable
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.