Konva Drag & Snap Shapes — Grid-Snapping Canvas Objects Snippet

Konva Drag & Snap Shapes · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Snap-on-release
dragend rounds position to the nearest grid multiple; dragmove is untouched.
Separate grid and shape layers
Static guide lines never redraw when shapes move.
Reusable makeDraggable helper
One function wires drag, snap, and cursor feedback onto any shape.
Z-order lift on drag
moveToTop() keeps the actively dragged shape above the rest.
Opacity drag cue
A subtle opacity dip signals which shape is being moved.
batchDraw for coalesced repaints
Multiple redraw requests in one frame collapse into a single repaint.
Cursor feedback
grab/grabbing cursor states follow pointer state on the shape.
Four distinct shape types
Circle, Rect, Star, and RegularPolygon all use the same drag pipeline.

About this UI Snippet

Konva Drag & Snap Shapes — Snapping on Release, Not Mid-Drag

Screenshot of the Konva Drag & Snap Shapes snippet rendered live

Grid-snapping feels obvious until you implement it: snap too eagerly and the shape jitters under the pointer, fighting the user's hand; snap too late and it never feels precise. Konva's event model — separate dragmove and dragend events per shape — makes the right answer easy: follow the pointer exactly while dragging, and snap only once, on release.

Two layers, two responsibilities

The stage holds a gridLayer (static guide lines, drawn once) and a shapeLayer (the draggable shapes). Konva layers are each backed by their own <canvas> element, so redrawing the shape layer during a drag never touches the grid layer's pixels — the browser only repaints what changed. This is the core reason Konva scales to more complex scenes than manually managing one shared canvas: retained shapes on separate layers let you redraw selectively instead of clearing and redrawing everything on every frame.

The snap math

function snap(value) { return Math.round(value / GRID) * GRID; }

Dividing by the grid size, rounding to the nearest integer, and multiplying back is the standard "round to nearest multiple" formula. Applied independently to x and y in the dragend handler, it moves the shape to the nearest grid intersection regardless of which direction it was dragged from.

Why snapping happens on dragend, not dragmove

shape.on('dragend', function () { shape.position({ x: snap(shape.x()), y: snap(shape.y()) }); ... });

If this logic ran on dragmove instead, the shape's position would be overwritten with a snapped value on every pointer-move event while dragging — visually, the shape would jump between grid points rather than following the cursor, which reads as broken rather than assistive. Running it only once, in dragend, gives you the best of both: free-form movement while the mouse is down, a satisfying settle onto the grid the instant it's released.

Drag feedback

dragstart calls shape.moveToTop() so the shape being dragged always renders above the others (Konva's z-order follows each layer's internal child array, and moveToTop() moves the node to the end of it), and drops its opacity to 0.85 as a lightweight "lifted" cue, both reverted in dragend.

batchDraw vs draw

Event handlers call shapeLayer.batchDraw() rather than .draw(). batchDraw schedules a redraw on the next animation frame and coalesces multiple calls within the same frame into one actual repaint, which matters once several shapes could be moving or updating within the same tick — .draw() forces an immediate synchronous repaint every single call.

Build with AI

Build, Understand, Optimize, and Extend It With AI

The core design decision in this snippet is timing \u2014 snapping on dragend rather than dragmove \u2014 so start a conversation there. Paste the code into an AI assistant like Claude and ask it to explain what visibly breaks if the snap logic moves into a dragmove handler instead, and why Konva's separate dragstart/dragmove/dragend events make that choice trivial to express compared to a raw canvas implementation tracking pointer state by hand. Then ask about the gridLayer/shapeLayer split \u2014 why keeping static content on a separate Konva layer from animated or draggable content is a broadly useful pattern for canvas performance. To extend it: add collision detection so shapes can't snap onto an already-occupied grid cell, persist shape positions to localStorage and restore them on load, add a right-click context menu to delete a shape, or make the grid size adjustable via a live slider that redraws the guide lines.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a canvas of draggable shapes that snap to a grid on release, using Konva.js (v9, from a CDN) in plain HTML, CSS, and JavaScript.

Requirements:
- A Konva.Stage with two Konva.Layer instances: one holding a static background grid of thin lines spaced by a GRID constant (drawn once with a loop over Konva.Line objects), and one holding the draggable shapes \u2014 kept separate so dragging a shape never triggers a redraw of the grid layer.
- At least 4 different Konva shape types (e.g. Circle, Rect, Star, RegularPolygon), each created with draggable(true) and a drop shadow, initially positioned already aligned to the grid.
- A reusable makeDraggable(shape) helper wiring: dragstart (moveToTop() plus a slight opacity reduction as a "lifted" visual cue), and dragend (round shape.x()/shape.y() to the nearest multiple of GRID using Math.round(value / GRID) * GRID, apply it with shape.position(), and restore full opacity). Crucially, do NOT snap during dragmove \u2014 the shape must follow the pointer smoothly while dragging and only snap once, on release.
- Cursor feedback: grab cursor on hover, grabbing while actively dragging.
- Use layer.batchDraw() rather than layer.draw() in the event handlers for efficient batched repaints.
- Style it as a dark canvas panel with a subtle grid and colorful shapes with soft shadows, inside a bordered, shadowed container.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="kds-stage">
  <div class="kds-head">
    <span class="kds-tag">Konva · dragend snapping</span>
    <h2>Drag &amp; Snap Shapes</h2>
    <p>Drag any shape — it snaps to the nearest grid intersection when you let go.</p>
  </div>
  <div id="kdsContainer" class="kds-container"></div>
</div>

Step by step

How to Use

  1. 1
    Add the Konva CDNInclude konva.min.js from the CDN panel — it attaches a global Konva object.
  2. 2
    Paste HTML, CSS, and JSA stage renders with a guide grid and four draggable shapes.
  3. 3
    Drag any shapeIt follows the pointer freely, lifting slightly and moving to the top.
  4. 4
    Release to snapdragend rounds x and y to the nearest grid intersection.
  5. 5
    Add more shapesCreate any Konva shape, call makeDraggable(shape), and add it to shapeLayer.
  6. 6
    Change the grid sizeEdit the GRID constant — both the drawn lines and the snap math read from it.

Real-world uses

Common Use Cases

Diagram / flowchart editors
Grid-aligned nodes for wireframing or flow diagrams.
Floor plan / layout tools
Snap furniture or fixtures to a design grid.
Puzzle / game boards
Piece placement that always lands on a valid cell.
Teaching Konva layers
A concrete reference for layer separation and batchDraw.
Dashboard widget arranging
Grid-snapped repositioning for draggable dashboard tiles.

Got questions?

Frequently Asked Questions

Snapping on every dragmove event would overwrite the shape\u2019s position with a rounded value on each pointer move, making it visibly jump between grid points instead of following the cursor. Snapping only once, on dragend, keeps movement smooth while dragging and settles the shape precisely on release.

Each Konva layer is its own canvas element. Keeping static grid lines on gridLayer means dragging a shape only triggers a redraw of shapeLayer \u2014 the grid\u2019s canvas is never touched, which is cheaper than redrawing both together.

Math.round(value / GRID) * GRID divides the coordinate by the grid size, rounds to the nearest whole number of grid units, then multiplies back \u2014 the standard formula for rounding a number to the nearest multiple of another.

draw() forces an immediate synchronous repaint on every call. batchDraw() schedules a redraw for the next animation frame and merges multiple calls within that frame into a single repaint, which is more efficient when several updates happen close together.

Replace the snap function with coordinate-specific math for that grid \u2014 for example, converting to axial hex coordinates, rounding those, and converting back \u2014 while keeping the same dragend-only trigger pattern.

In dragend, after computing the snapped position, check shapeLayer.getIntersection or iterate the other shapes\u2019 positions for a collision; if the target cell is occupied, revert to the shape\u2019s previous snapped position instead of applying the new one.