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 { createSignal } from 'solid-js';
import { grid, useDraggable } from '@neodrag/solid';
function GridExamples() {
const [square, setSquare] = createSignal<HTMLElement | null>(null);
const [rect, setRect] = createSignal<HTMLElement | null>(null);
const [horizontal, setHorizontal] =
createSignal<HTMLElement | null>(null);
const [vertical, setVertical] = createSignal<HTMLElement | null>(
null,
);
useDraggable(square, [grid([20, 20])]);
useDraggable(rect, [grid([50, 30])]);
useDraggable(horizontal, [grid([25, null])]);
useDraggable(vertical, [grid([null, 20])]);
return (
<div>
<div ref={setSquare}>Snaps to 20px squares</div>
<div ref={setRect}>Snaps to 50x30px rectangles</div>
<div ref={setHorizontal}>Only snaps horizontally</div>
<div ref={setVertical}>Only snaps vertically</div>
</div>
);
}
Dynamic Grid Sizing
import { createSignal } from 'solid-js';
import {
grid,
useDraggable,
createCompartment,
} from '@neodrag/solid';
function DynamicGrid() {
const [element, setElement] = createSignal<HTMLElement | null>(
null,
);
const [gridSize, setGridSize] = createSignal(20);
const [enableSnap, setEnableSnap] = createSignal(true);
const gridComp = createCompartment(() =>
enableSnap()
? grid([gridSize(), gridSize()])
: grid([null, null]),
);
useDraggable(element, [gridComp]);
return (
<div>
<div ref={setElement}>
Grid: {enableSnap() ? `${gridSize()}px` : 'disabled'}
</div>
<label>
Grid Size: {gridSize()}px
<input
type="range"
value={gridSize()}
onInput={(e) => setGridSize(Number(e.target.value))}
min="5"
max="50"
step="5"
/>
</label>
<label>
<input
type="checkbox"
checked={enableSnap()}
onChange={(e) => setEnableSnap(e.target.checked)}
/>
Enable Grid Snap
</label>
</div>
);
}
Advanced Grid Patterns
Asymmetric Grids
import { createSignal } from 'solid-js';
import { grid, useDraggable } from '@neodrag/solid';
function AsymmetricGrids() {
const [wide, setWide] = createSignal<HTMLElement | null>(null);
const [timeline, setTimeline] = createSignal<HTMLElement | null>(
null,
);
const [calendar, setCalendar] = createSignal<HTMLElement | null>(
null,
);
useDraggable(wide, [grid([100, 20])]);
useDraggable(timeline, [grid([60, null])]);
useDraggable(calendar, [grid([80, 120])]);
return (
<div>
<div ref={setWide}>100x20px grid</div>
<div ref={setTimeline}>60px intervals (hourly)</div>
<div ref={setCalendar}>Daily x Weekly</div>
</div>
);
}
Responsive Grid System
import { createSignal, onMount, onCleanup } from 'solid-js';
import {
grid,
useDraggable,
createCompartment,
} from '@neodrag/solid';
function ResponsiveGrid() {
const [element, setElement] = createSignal<HTMLElement | null>(
null,
);
const [screenWidth, setScreenWidth] = createSignal(
window.innerWidth,
);
// 12-column responsive grid
const columnWidth = () => screenWidth() / 12;
const responsiveGrid = createCompartment(() =>
grid([columnWidth(), 40]),
);
const handleResize = () => setScreenWidth(window.innerWidth);
onMount(() => {
window.addEventListener('resize', handleResize);
});
onCleanup(() => {
window.removeEventListener('resize', handleResize);
});
useDraggable(element, [responsiveGrid]);
return (
<div ref={setElement}>
Responsive 12-column grid
<br />
Column: {Math.round(columnWidth())}px
</div>
);
}
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.