Skip to content

Instantly share code, notes, and snippets.

@mStirner
Created December 11, 2024 19:46
Show Gist options
  • Save mStirner/53cc6ad6c052ba4dcc3c678324311d2d to your computer and use it in GitHub Desktop.
Save mStirner/53cc6ad6c052ba4dcc3c678324311d2d to your computer and use it in GitHub Desktop.
HTML/CSS Drag & Resziable element.

This gist allows to define a css grid system where elements can be positioned inside. E.g. Dashboard widget arrangement.

Add a dragable element, resize it, position where ever you want.

Screenshot 2024-12-11 at 20-45-55 Grid Draggable   Resizable Element

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grid Draggable & Resizable Element</title>
<style>
*::selection {
background-color: transparent;
user-select: none;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
.grid {
display: grid;
gap: 2px;
border: 2px solid #000;
position: relative;
background-color: #000;
}
.grid div {
background-color: #e0e0e0;
}
.draggable {
width: 100px;
height: 100px;
cursor: grab;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
background-color: red !important;
/*resize: both;*/
overflow: auto;
box-sizing: border-box;
}
.draggable:active {
cursor: grabbing;
}
.disabled {
pointer-events: none;
opacity: 0.6;
}
div.dashed-border{
border: 1px dashed #fff;
}
</style>
</head>
<body>
<div class="grid" id="grid"></div>
<div style="margin-top: 20px; text-align: center;">
<label>Columns:
<input type="number" id="nCols" min="1" value="5">
</label>
<label>Rows:
<input type="number" id="nRows" min="1" value="5">
</label>
<button id="updatePosition">Update Grid</button>
<button id="addDraggable" disabled>Add Draggable</button>
<label>
<input type="checkbox" id="toggleLock"> Enable Dragging/Resizing
</label>
</div>
<script>
const updateButton = document.getElementById('updatePosition');
const addDraggableButton = document.getElementById('addDraggable');
const toggleLock = document.getElementById('toggleLock');
const nCols = document.getElementById('nCols');
const nRows = document.getElementById('nRows');
const grid = document.getElementById('grid');
const draggableElements = [];
const setDefaultGrid = () => {
updateGrid(5, 5);
createDraggable(0, 0);
};
toggleLock.addEventListener("click", (e) => {
if (toggleLock.checked) {
// enable dragging
addDraggableButton.disabled = false;
draggableElements.forEach(({element}) => {
console.log("element", element)
element.style.resize = "both";
element.classList.add("dashed-border");
});
} else {
//disable dragging
addDraggableButton.disabled = true;
draggableElements.forEach(({element}) => {
element.style.resize = "none";
element.classList.remove("dashed-border");
});
}
});
const updateGrid = (cols, rows) => {
grid.style.gridTemplateColumns = `repeat(${cols}, 100px)`;
grid.style.gridTemplateRows = `repeat(${rows}, 100px)`;
// Clear grid cells (but not draggable elements)
const totalCells = cols * rows;
while (grid.firstChild && !grid.firstChild.classList.contains('draggable')) {
grid.removeChild(grid.firstChild);
}
for (let i = 0; i < totalCells; i++) {
const cell = document.createElement('div');
grid.appendChild(cell);
}
// Reattach draggable elements
draggableElements.forEach(({ element, left, top, width, height }) => {
element.style.left = `${left}px`;
element.style.top = `${top}px`;
element.style.width = `${width}px`;
element.style.height = `${height}px`;
grid.appendChild(element);
});
};
const createDraggable = (left = 0, top = 0) => {
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
const draggable = document.createElement('div');
draggable.className = 'draggable';
draggable.textContent = 'Drag Me';
draggable.style.left = `${left}px`;
draggable.style.top = `${top}px`;
if(toggleLock.checked){
draggable.style.resize = "both";
// add only if resize checkbox is enabled
// if this is called in "setDefaultGrid()" this indicates its dragable, which is by default false
// thats the reason why is only added if resize is set & checkbox enabled
draggable.classList.add("dashed-border");
}else{
draggable.style.resize = "none";
}
const elementData = {
element: draggable,
left,
top,
width: 100,
height: 100,
};
draggableElements.push(elementData);
grid.appendChild(draggable);
draggable.addEventListener('mousedown', (e) => {
if (toggleLock.checked) {
isDragging = true;
offsetX = e.clientX - draggable.offsetLeft;
offsetY = e.clientY - draggable.offsetTop;
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging && toggleLock.checked) {
let x = e.clientX - offsetX;
let y = e.clientY - offsetY;
// Snap to grid
const gridSize = 100 + 2; // 100px cell + 5px gap
x = Math.round(x / gridSize) * gridSize;
y = Math.round(y / gridSize) * gridSize;
// Constrain within grid boundaries
x = Math.max(0, Math.min(x, grid.offsetWidth - draggable.offsetWidth));
y = Math.max(0, Math.min(y, grid.offsetHeight - draggable.offsetHeight));
draggable.style.left = `${x}px`;
draggable.style.top = `${y}px`;
elementData.left = x;
elementData.top = y;
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Ensure resizing snaps to grid
const enforceGridSize = () => {
if (toggleLock.checked) {
const gridSize = 100 + 2; // 100px cell + 5px gap
let width = draggable.offsetWidth;
let height = draggable.offsetHeight;
// Calculate the closest valid grid-aligned size
const newWidth = Math.round(width / gridSize) * gridSize - 2;
const newHeight = Math.round(height / gridSize) * gridSize - 2;
// Enforce minimum size and reset position to prevent shifting
draggable.style.width = `${Math.max(gridSize - 5, newWidth)}px`;
draggable.style.height = `${Math.max(gridSize - 5, newHeight)}px`;
let left = parseInt(draggable.style.left, 10);
let top = parseInt(draggable.style.top, 10);
left = Math.round(left / gridSize) * gridSize;
top = Math.round(top / gridSize) * gridSize;
draggable.style.left = `${left}px`;
draggable.style.top = `${top}px`;
elementData.width = newWidth;
elementData.height = newHeight;
elementData.left = left;
elementData.top = top;
}
};
draggable.addEventListener('mouseup', enforceGridSize);
draggable.addEventListener('mouseleave', enforceGridSize);
};
setDefaultGrid();
updateButton.addEventListener('click', () => {
const cols = parseInt(nCols.value, 10);
const rows = parseInt(nRows.value, 10);
if (cols > 0 && rows > 0) {
updateGrid(cols, rows);
} else {
alert('Columns and rows must be greater than 0.');
}
});
addDraggableButton.addEventListener('click', () => {
createDraggable(0, 0);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment