grid

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:

  1. Calculate grid position - Uses Math.ceil(value / gridSize) * gridSize to find the next grid point
  2. Apply to proposed movement - Modifies ctx.proposed.x and ctx.proposed.y
  3. Null values skip snapping - null means free movement on that axis
  4. 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 dimensions
    • number - Grid size in pixels for that axis
    • null - No grid snapping for that axis

Usage patterns:

  • grid([20, 20]) - Square 20px grid
  • grid([50, 30]) - Rectangular 50x30px grid
  • grid([25, null]) - Horizontal snapping only
  • grid([null, 15]) - Vertical snapping only
  • grid([null, null]) - No snapping (effectively disabled)

Returns: A plugin object for use with draggable.