---
title: "Design canvas"
description: "A Figma-like editor — frames and layers on a zoomable canvas, smart guides, a rubber band, Alt to duplicate, and a layers panel that is the design's tree."
canonical_url: "https://zizigy.github.io/vue-dnd-kit/docs/examples/design-canvas"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

<example height="43rem" hint="Drag a layer into the other frame; draw a box, Shift-click, hold Alt to drop copies" name="design-canvas" title="Design canvas">



</example>

## What to look at

- **Shape.vue** and **Artboard.vue**: every layer is a plain draggable, and a frame drags by
its name (`handle`), its layers inside it. A press selects at once, as in Figma, so the
provider's selection is what a drag takes: press one of several selected layers and they all
move.
- **useLayerDrop.ts**: the canvas and every frame are drop targets that work alike. A layer let
go over a frame moves into it; over the canvas, out of it. Hold <kbd value="Alt">



</kbd>

 and copies
land instead, the layers staying where they were.
- **LayersPanel.vue**: the panel is `makeSortableTree` on the design itself. The rows' order is
the stacking order, and dropping a row into a frame moves the layer into that frame.
- **Canvas.vue**: the smart guides are a modifier of the demo's own; `makeSelectionArea` draws
the rubber band, and `makeDragArea` gives every drag that starts on the canvas its rules.
- **editor.ts**, **geometry.ts** and **design.ts**: the design, its history, the view and
the maths of snapping and aligning, with no drag and drop in them.

## Drawn at the zoom

The canvas zooms without CSS `scale()`: every layer is laid out at its size times the zoom, in
screen pixels. What the library measures is then what is on screen, so the drop targets, the
rubber band and the clones in the overlay need nothing special. The world inside the canvas is
moved by the pan alone, with `translate()`.

```ts [Shape.vue]
const style = computed(() => {
  const { zoom } = editor.view;
  const { x, y, width, height } = props.shape;
  return { left: `${x * zoom}px`, top: `${y * zoom}px`, width: `${width * zoom}px`, height: `${height * zoom}px` };
});
```

The wheel pans and zooms (<kbd value="Ctrl">



</kbd>

 or a pinch), and so do <kbd value="Space">



</kbd>

-drag, the
hand tool and the buttons in the corner: plain pointer and wheel events, as are the resize
handles. Space being the hand, a layer picks up with <kbd value="Enter">



</kbd>

 alone (the provider's
`keyboard.keys`).

## Moving layers between frames

A layer is a plain draggable with an overlay of its own, `DragGroup.vue`. It lays the dragged
layers out as they lie: a `DragPreview` (the clone taken at pickup) of each, placed from the
`data` of its layer, where it is drawn. The layers themselves step aside while their clones
move, unless <kbd value="Alt">



</kbd>

 is held.

```ts [Shape.vue]
const { isDragging, selected } = makeDraggable(el, {
  id: () => props.shape.id,
  type: 'layer',
  data: () => editor.dragData(props.shape), // { layer, left, top }
  overlay: DragGroup,
});
```

On drop, `position` is where the grabbed layer's corner was let go inside the target, in screen
pixels. Divided by the zoom, it says how far that layer went in the world, and every dragged
layer moves as far, into the frame or onto the canvas:

```ts [useLayerDrop.ts]
const corner = target ? editor.boxOf(target) : { x: -view.x / view.zoom, y: -view.y / view.zoom };
const toWorld = (x: number, y: number) => ({ x: corner.x + x / view.zoom, y: corner.y + y / view.zoom });

const from = editor.boxOf(grabbed); // the layer of `active.id`
const to = toWorld(position.x, position.y);
editor.move(active.ids, { x: to.x - from.x, y: to.y - from.y }, target, editor.altHeld.value);
```

A frame takes layers, not frames: a drag that carries a frame passes over the other frames
(`disabled`, read live) onto the canvas. The targets have `overDropAnimation: false`, as the layers
appear right where their clones were let go. Tools in the toolbar are plain draggables too:
dropped on a frame, they add their layer into it, centred on the drop point; clicked, in the
middle of the view.

```ts [Artboard.vue]
makeDroppable(body, {
  id: () => props.frame.id,
  accept: 'layer',
  disabled: () => !!active.value?.items.some((item) => item.type.includes('frame')),
  overDropAnimation: false,
  onDrop: useLayerDrop(() => props.frame),
});
```

## Smart guides

`smartGuides` is a modifier: it gets the drag's offset, works out where the dragged layers would
be in the world, and pulls them onto the closest edge or centre of another layer within 6 screen
pixels. The overlay, the target under it and the drop all follow. A monitor draws the pink
lines and the distances from the drag's `delta`, the offset after the modifiers.

```ts [Canvas.vue]
const smartGuides: Modifier = ({ transform, active }) => {
  const moving = editor.boundsOf(active.ids);
  if (!moving) return transform; // a tool from the toolbar
  const { zoom } = view;
  const box = offsetBox(moving, transform.x / zoom, transform.y / zoom);
  const targets = editor.snapTargets(active.ids);
  return {
    x: transform.x + snapBox(box, targets, 'x', SNAP / zoom) * zoom,
    y: transform.y + snapBox(box, targets, 'y', SNAP / zoom) * zoom,
  };
};

// Every drag that starts on the canvas, frames and layers alike, gets the guides.
makeDragArea(viewport, { modifiers: [smartGuides], disabled: panning });
```

The resize handles snap with the same lines and draw the same guides.

## Selecting

A rubber band drawn on the empty canvas selects frames and loose layers; drawn inside a frame,
the frame's layers. Both are `makeSelectionArea`: the innermost area takes the press, and owns
the draggables inside it. <kbd value="Shift">



</kbd>

 adds to the selection (`strategy: 'toggle'`).

```ts [Artboard.vue]
const { isSelecting, style: band } = makeSelectionArea(body, {
  strategy: () => (editor.shiftHeld.value ? 'toggle' : 'select'),
  disabled: editor.panning,
});
```

The editor never selects a frame together with a layer inside it: dragged, the layer would
move twice. A band captures the pointer as it starts, so a click on a frame's empty space never
reaches the frame: `Artboard.vue` selects the frame when a press there is released where it
started.

## The layers panel

The tree's items are the design: frames with their `children`, the top row in front. `canNest`
lets frames take layers and keeps frames at the top level. Rows have keys of their own
(`row:…`), as the layers' ids belong to their draggables on the canvas; the selection is the
canvas's, so a row lights up with its layer.

```ts [LayersPanel.vue]
makeSortableTree(tree, {
  items: editor.layers,
  getKey: (layer) => `row:${layer.id}`,
  canNest: (layer, parent) => parent.kind === 'frame' && layer.kind !== 'frame',
  maxDepth: 1,
  animateChanges: true,
  onDrop: ({ items, from, to }) => {
    // Positions count from the frame's corner: a layer changing frames takes the
    // difference, so it stays where it is on the canvas.
  },
});
```

`onDrop` runs before the tree is written, so the editor still knows the frame each layer comes
from, and saves the design for undo. A layer moved into a frame that doesn't cover it would be
clipped out of sight: it goes to the frame's middle instead. With `animateChanges`, rows slide for
changes made on the canvas and by undo too. A row is typed (`type: 'row'`), so the canvas and
the frames, which take layers and tools, refuse it.

## Keyboard

Tab to a layer (it gets selected), then <kbd value="Enter">



</kbd>

 picks it up, the arrows move it 10
pixels (<kbd value="Shift">



</kbd>

: 40), and <kbd value="Enter">



</kbd>

 drops it in the frame under it. Without
a drag, the arrows nudge the selection by a pixel. A tool picks up the same way, and its arrows
jump between the canvas and the frames (`keyboardMode: 'droppables'`). The other shortcuts are listed
in the Design panel when nothing is selected.

Read more: [Drag and drop](https://zizigy.github.io/docs/guides/drag-and-drop#leave-it-where-it-was-dropped) ·
[Your own modifier](https://zizigy.github.io/docs/guides/constraints#your-own-modifier) ·
[Rules for a part of the page](https://zizigy.github.io/docs/guides/constraints#rules-for-a-part-of-the-page) ·
[Rubber-band selection](https://zizigy.github.io/docs/guides/multi-drag#rubber-band-selection) ·
[Trees](https://zizigy.github.io/docs/guides/trees)

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
