Skip to main content

Infinite Canvas with Zoom and Pan — Build with JavaScript

An infinite canvas lets users pan and zoom freely across a boundless workspace — the foundation of tools like Figma, Miro, and tldraw. This demo shows how to build one with Konva: scroll to zoom relative to your cursor, drag empty space to pan, and drag shapes to move them.

Instructions: Scroll to zoom. Drag the background to pan. Drag shapes to move them.

import Konva from 'konva';

var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
  draggable: true,
});

var layer = new Konva.Layer();
stage.add(layer);

// Generate grid of dots
var gridSpacing = 40;
var gridRange = 2000;
for (var x = -gridRange; x <= gridRange; x += gridSpacing) {
  for (var y = -gridRange; y <= gridRange; y += gridSpacing) {
    layer.add(new Konva.Circle({
      x: x,
      y: y,
      radius: 1,
      fill: '#ccc',
      listening: false,
    }));
  }
}

// Scatter colorful shapes across the space
var shapeDefs = [
  { type: 'rect', x: 80, y: 60, width: 120, height: 80, fill: '#FF6B6B', rotation: 5 },
  { type: 'circle', x: 350, y: 120, radius: 50, fill: '#4ECDC4' },
  { type: 'rect', x: 600, y: -80, width: 90, height: 90, fill: '#45B7D1', rotation: -10 },
  { type: 'star', x: -150, y: 250, numPoints: 5, innerRadius: 20, outerRadius: 45, fill: '#FFE66D' },
  { type: 'circle', x: -400, y: -200, radius: 65, fill: '#DDA0DD' },
  { type: 'rect', x: 200, y: -350, width: 140, height: 60, fill: '#98D8C8', rotation: 15 },
  { type: 'star', x: 500, y: 300, numPoints: 6, innerRadius: 25, outerRadius: 55, fill: '#F7DC6F' },
  { type: 'circle', x: -300, y: 450, radius: 40, fill: '#82E0AA' },
  { type: 'rect', x: -550, y: 100, width: 100, height: 100, fill: '#F1948A', rotation: -20 },
  { type: 'star', x: 750, y: -250, numPoints: 5, innerRadius: 30, outerRadius: 60, fill: '#AED6F1' },
  { type: 'circle', x: 100, y: 550, radius: 55, fill: '#D2B4DE' },
  { type: 'rect', x: -100, y: -500, width: 110, height: 70, fill: '#A3E4D7', rotation: 8 },
];

shapeDefs.forEach(function(d) {
  var shape;
  if (d.type === 'rect') {
    shape = new Konva.Rect({
      x: d.x, y: d.y, width: d.width, height: d.height,
      fill: d.fill, cornerRadius: 8, rotation: d.rotation || 0,
      draggable: true, shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4,
    });
  } else if (d.type === 'circle') {
    shape = new Konva.Circle({
      x: d.x, y: d.y, radius: d.radius,
      fill: d.fill, draggable: true,
      shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4,
    });
  } else {
    shape = new Konva.Star({
      x: d.x, y: d.y, numPoints: d.numPoints,
      innerRadius: d.innerRadius, outerRadius: d.outerRadius,
      fill: d.fill, draggable: true,
      shadowColor: 'rgba(0,0,0,0.15)', shadowBlur: 10, shadowOffsetY: 4,
    });
  }
  layer.add(shape);
});

// Zoom relative to pointer
var scaleBy = 1.05;
stage.on('wheel', function(e) {
  e.evt.preventDefault();
  var oldScale = stage.scaleX();
  var pointer = stage.getPointerPosition();
  var mousePointTo = {
    x: (pointer.x - stage.x()) / oldScale,
    y: (pointer.y - stage.y()) / oldScale,
  };
  var direction = e.evt.deltaY > 0 ? -1 : 1;
  if (e.evt.ctrlKey) { direction = -direction; }
  var newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
  newScale = Math.max(0.1, Math.min(10, newScale));
  stage.scale({ x: newScale, y: newScale });
  var newPos = {
    x: pointer.x - mousePointTo.x * newScale,
    y: pointer.y - mousePointTo.y * newScale,
  };
  stage.position(newPos);
});