grid
Snap movement to a grid pattern
The grid
plugin snaps draggable movement to a grid pattern. Instead of smooth, continuous movement, elements jump between grid points - perfect for alignment, layout tools, or pixel-perfect positioning.
grid([20, 20]); // 20px square grid
grid([50, 25]); // 50px wide, 25px tall rectangles
grid([10, null]); // Only snap horizontally every 10px
grid([null, 15]); // Only snap vertically every 15px
Basic Usage
import { grid, Draggable } from '@neodrag/vanilla';
const square = document.getElementById('square');
const rect = document.getElementById('rect');
const horizontal = document.getElementById('horizontal');
const vertical = document.getElementById('vertical');
new Draggable(square, [grid([20, 20])]);
new Draggable(rect, [grid([50, 30])]);
new Draggable(horizontal, [grid([25, null])]);
new Draggable(vertical, [grid([null, 20])]);
Dynamic Grid Sizing
import { grid, Draggable, Compartment } from '@neodrag/vanilla';
let gridSize = 20;
let enableSnap = true;
const gridComp = new Compartment(() =>
enableSnap ? grid([gridSize, gridSize]) : grid([null, null]),
);
const element = document.getElementById('element');
const sizeSlider = document.getElementById('size-slider');
const snapToggle = document.getElementById('snap-toggle');
const statusEl = document.getElementById('status');
new Draggable(element, () => [gridComp]);
function updateStatus() {
statusEl.textContent = enableSnap ? `${gridSize}px` : 'disabled';
gridComp.current = enableSnap
? grid([gridSize, gridSize])
: grid([null, null]);
}
sizeSlider.addEventListener('input', (e) => {
gridSize = Number(e.target.value);
updateStatus();
});
snapToggle.addEventListener('change', (e) => {
enableSnap = e.target.checked;
updateStatus();
});
Advanced Grid Patterns
Asymmetric Grids
import { grid, Draggable } from '@neodrag/vanilla';
const wide = document.getElementById('wide');
const timeline = document.getElementById('timeline');
const calendar = document.getElementById('calendar');
new Draggable(wide, [grid([100, 20])]);
new Draggable(timeline, [grid([60, null])]);
new Draggable(calendar, [grid([80, 120])]);
Responsive Grid System
import { grid, Draggable, Compartment } from '@neodrag/vanilla';
let screenWidth = window.innerWidth;
const getColumnWidth = () => screenWidth / 12;
const responsiveGrid = new Compartment(() =>
grid([getColumnWidth(), 40]),
);
const element = document.getElementById('element');
const statusEl = document.getElementById('status');
new Draggable(element, () => [responsiveGrid]);
function handleResize() {
screenWidth = window.innerWidth;
responsiveGrid.current = grid([getColumnWidth(), 40]);
statusEl.textContent = `Column: ${Math.round(getColumnWidth())}px`;
}
window.addEventListener('resize', handleResize);
Real-World Examples
Pixel Art Editor
// Precise 1px grid for pixel-perfect editing
const pixelGrid = [grid([1, 1]), bounds(BoundsFrom.element(canvas))];
Card Layout System
// Cards snap to a 12-column grid system
const containerWidth = 1200;
const columnWidth = containerWidth / 12;
const cardGrid = [
grid([columnWidth, 40]), // 12-column, 40px row height
bounds(BoundsFrom.parent()),
];
Timeline Editor
// Timeline with 15-minute intervals
const minutesPerPixel = 0.25; // 15 minutes = 60 pixels
const timelineGrid = [
grid([60, null]), // 15-minute intervals horizontally
bounds(BoundsFrom.element(timelineContainer)),
];
Icon Grid Layout
// Desktop-style icon grid
const iconSize = 64;
const iconSpacing = 80;
const iconGrid = [
grid([iconSpacing, iconSpacing]),
bounds(BoundsFrom.viewport({ top: 100, bottom: 50 })),
];
How It Works
The grid plugin modifies movement by snapping to grid points:
- Calculate grid position - Uses
Math.ceil(value / gridSize) * gridSize
to find the next grid point - Apply to proposed movement - Modifies
ctx.proposed.x
andctx.proposed.y
- Null values skip snapping -
null
means free movement on that axis - Runs during drag - Continuously snaps while dragging
The snapping formula ensures elements always move forward to the next grid intersection, creating predictable, aligned positioning.
API Reference
function grid(size: [number | null, number | null]): Plugin;
Parameters:
size
- Array with[x, y]
grid dimensionsnumber
- Grid size in pixels for that axisnull
- No grid snapping for that axis
Usage patterns:
grid([20, 20])
- Square 20px gridgrid([50, 30])
- Rectangular 50x30px gridgrid([25, null])
- Horizontal snapping onlygrid([null, 15])
- Vertical snapping onlygrid([null, null])
- No snapping (effectively disabled)
Returns: A plugin object for use with draggable.