TLDR; View the working example on jsfiddle.net
Let’s create a solution that resizes a div at fixed 50px increments of its parent container, with smooth transitions, debouncing and ResizeObserver for monitoring.
HTML Setup:
<div class="container">
<div class="resizable"></div>
</div>
CSS Setup:
.container {
width: 500px;
height: 400px;
border: 2px solid #ccc;
position: relative;
overflow: hidden;
}
.resizable {
width: 250px;
height: 200px;
background: #3498db;
position: absolute;
resize: both; /* This enables the resizing */
overflow: auto;
min-width: 50px;
min-height: 50px;
max-width: 100%;
max-height: 100%;
transition: all 0.1s ease-out; /* This is important to ensure a smooth drag */
}
resize: bothenables resizing in both directionstransition: all 0.1s ease-outadds smooth transitions to reduce visual glitchingmin-width/height: 10%andmax-width/height: 100%set the boundariesoverflow: autohandles content overflow- Container has
overflow: hiddento prevent content spilling
Javascript Setup:
const resizable = document.querySelector(".resizable")
const container = document.querySelector(".container")
const snapPercent = 20 // Change to fit your needs
const snapPixel = 50 // Change to fit your needs
const usePixels = false // Change this to false to use percentage.
// Function to snap to fixed pixels increments
function snapToGridPixel(size, parentSize) {
const snappedSize = Math.round(size / snapPixel) * snapPixel
const value = Math.max(snapPixel, Math.min(snappedSize, parentSize))
return Math.min(value, parentSize) + "px"
}
// Function to snap to fixed percentage increments
function snapToGridPercentage(size, parentSize) {
const percentage = (size / parentSize) * 100
const snappedPercentage = Math.round(percentage / snapPercent) * snapPercent
return Math.max(snapPercent, Math.min(100, snappedPercentage)) + "%"
}
function snapToGrid(size, parentSize) {
return usePixels
? snapToGridPixel(size, parentSize)
: snapToGridPercentage(size, parentSize)
}
// Debounce function
function debounce(func, delay) {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
// Debounced resize handler
const handleResize = debounce((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect
const parentWidth = container.offsetWidth
const parentHeight = container.offsetHeight
// Snap to fixed increments
const newWidth = snapToGrid(width, parentWidth)
const newHeight = snapToGrid(height, parentHeight)
// Apply the snapped sizes with max bounds
resizable.style.width = newWidth
resizable.style.height = newHeight
}
}, 100) // 100ms delay
// ResizeObserver with debounced handler
const resizeObserver = new ResizeObserver(handleResize)
// Start observing the resizable element
resizeObserver.observe(resizable)
Key Components Explained
ResizeObserver
The ResizeObserver continuously monitors size changes and applies snapping whenever the user resizes the div. It immediately snaps to the nearest increment, while the CSS transition makes the snapping motion smooth rather than instantaneous.
Snap to Grid Function
The snapToGrid function calculates the nearest fixed increment:
- Converts current size to percentage or pixels of parent
- Rounds to nearest increment using
Math.round(size / increment) * increment - Clamps values between minimum and maximum bounds
Debouncing
Debouncing ensures that a function only runs after a certain period of inactivity. It delays execution until after the triggering event has stopped firing for a specified amount of time.
Why Use Debouncing?
- Performance: Prevents excessive function calls that could slow down your application
- Efficiency: Reduces unnecessary computations or API calls
- User Experience: Ensures actions complete only when the user has finished interacting