Docs/Guides
Axes, bounds and grids
Lock drags to one axis, keep them inside a box, snap them to a grid or to guides of your own — for one item, a list or a whole part of the page.
A drag can be held to one axis, kept inside a box, snapped to a grid, or bent by a rule of your own. The rules are modifiers: they adjust the drag's offset on every frame. Set them on one draggable, on a list, or on a part of the page, and the overlay, the drop targets and the drop all follow them.
Lock an axis
Clips on a timeline slide along their track. Turn the lock off and they can change tracks too.
Loading the demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Timeline from './Timeline.vue';
</script>
<template>
<DnDProvider>
<Timeline />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.clip.id,
axis: () => (props.locked ? 'x' : undefined),
});
axis: 'x' moves only horizontally, 'y' only vertically. makeDraggable, makeSortableList,
makeSortableTree, makeSortableItem and makeDragArea all take it:
makeSortableList(listRef, { items: tracks, axis: 'y' });
- The lock runs after every modifier, so it holds whatever they do: a snap or a bound can't push the item off its line.
- The line is in the content, not on the screen: it moves with the list (or the parent) the drag started in. Scroll the page mid-drag and the item stays on its row.
- An item's own
axiswins over its list's or tree's, and a draggable's (or its list's) wins over its drag area's. - It is read when a drag starts, so a getter can toggle it, as above.
- A tree locked to
'y'still nests: the depth then comes from hovering the middle of a row.
axis to the restrictToVerticalAxis and restrictToHorizontalAxis modifiers. They do the
same thing, but as ordinary modifiers they run in the middle of the chain, and a modifier after
them (a grid snapping both axes) can move the item off the axis again.Modifiers
A modifier adjusts the drag's offset on every frame. Its result moves the overlay and is the
point where targets are looked for, so a list sorts by the locked, bounded or snapped position,
and the drop's position is where the box ended up.
| Modifier | What it does |
|---|---|
restrictToParent | Keeps the overlay inside the container the drag started in: the sortable list, or a plain draggable's parent element. |
restrictToElement(target) | Keeps the overlay inside an element: a template ref, a getter or the element itself. |
restrictToWindowEdges | Keeps the overlay inside the window. |
restrictToFirstScrollableAncestor | Keeps the overlay inside the first scrollable ancestor of the dragged element. |
snapToGrid(size) | Snaps the overlay to a grid (below). |
snapCenterToCursor | Centers the overlay under the pointer instead of keeping the grab offset. |
restrictToVerticalAxis | Moves only vertically. Prefer axis: 'y'. |
restrictToHorizontalAxis | Moves only horizontally. Prefer axis: 'x'. |
modifiers is an option of the provider, of drag areas, of lists and trees, and of draggables
and sortable items. Every one but the provider's also takes a ref or a getter, read when the
drag starts.
Keep it inside
A self view in a video call stays on the screen. Switch the bound and drag it far out:
Loading the demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import CallScreen from './CallScreen.vue';
</script>
<template>
<DnDProvider>
<CallScreen />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: 'self-view',
modifiers: () => props.modifiers, // [restrictToParent], [restrictToWindowEdges] or []
});
restrictToParentholds it on the screen, its parent element. For a sortable item, the "parent" is its list, wherever the list sits in the DOM.restrictToWindowEdgeslets it leave the screen but not the window. Dropped off the screen, it isn't on a drop target, so it flies back.- Without a bound it goes anywhere.
A bound keeps the overlay inside, at the size it has: with overlayFit: 'slot', a box
that grows over a list of wider items is kept inside at its new size.
Snap to a grid
The nodes of a flowchart snap to the canvas grid. Switch the cell size and the glide:
Loading the demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Flowchart from './Flowchart.vue';
</script>
<template>
<DnDProvider>
<Flowchart />
</DnDProvider>
</template>const { isDragging } = makeDraggable(el, {
id: () => props.node.id,
modifiers: () => [snapToGrid({ x: props.grid, y: props.grid, smooth: props.smooth })],
});
snapToGrid(24) snaps both axes to 24 px cells. An object sets more:
| Option | Meaning |
|---|---|
x, y | The cell size in pixels, per axis. Leave one out to snap the other only: { y: 15 } snaps an event to quarter hours and lets it move freely across the days. |
origin | 'container' (default): the overlay's top-left corner lands on grid lines drawn from the container's top-left — the sortable list, or a plain draggable's parent. 'delta': the offset itself is rounded, so an item that started off the grid stays off it and moves by whole cells from where it was. Pick it for items placed anywhere (a free canvas) that should keep their own alignment. |
smooth | true or { duration, easing }: the overlay glides between cells (80 ms by default). Targets use the snapped position at once. |
With origin: 'container', draw the grid from the container's border box: the canvas
above has its frame as a box-shadow, not a border, so its lines start at its very edge.
Your own modifier
Boxes on a slide jump onto the edges and centers of the others when they come close, and pink guides show the lines they sit on:
Loading the demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Slide from './Slide.vue';
</script>
<template>
<DnDProvider>
<Slide />
</DnDProvider>
</template>import type { Modifier } from '@vue-dnd-kit/core';
/** Near a line, the box jumps onto it. */
const magnet: Modifier = ({ transform, overlayRect, containerRect, active }) => {
if (!containerRect) return transform;
const id = String(active.id);
const left = overlayRect.left - containerRect.left;
const top = overlayRect.top - containerRect.top;
return {
x: transform.x + pull(left, overlayRect.width, lines(id, 'x')),
y: transform.y + pull(top, overlayRect.height, lines(id, 'y')),
};
};
// In order: the magnet pulls, then the slide's edges hold.
const modifiers = [magnet, restrictToParent];
A modifier gets transform, the offset from the drag start as the modifiers before it left
it, and returns a new one. It also gets what it may need to compute it:
| Argument | What it is |
|---|---|
transform | The offset so far. |
overlayRect | The overlay box moved by transform, at the size it has now. |
activeRect | The dragged element when the drag started. |
containerRect | The container the drag started in (the sortable list, or the parent element), where it is now: it follows scrolling. |
windowRect | The window. |
origin | The pointer when the drag started. |
active, over | The drag and the target under it. |
Modifiers run on every frame: keep them quick and free of side effects. The guides above
are drawn from the drag's delta in a monitor's onDragMove, which is the offset after
every modifier. Types in Modifiers.
The order
Modifiers from several places run one after another, each from the result of the one before:
- the provider's
modifiers; - the
modifiersof the drag area the drag started in; - the dragged item's own
modifiersor, when it has none, its list's or tree's; - the drag area's
restrictToArea; - the
axislock.
Within one array they run in order, so put snaps and magnets before bounds: a bound last
keeps the box inside whatever the others did. The slide above runs [magnet, restrictToParent];
the other way round, the magnet could pull a box held at the slide's edge a few pixels past it.
An item's modifiers replace its list's; they don't add to them. The area's bounds come after
the snapping of steps 1–3, so a snapped item still stays inside, and the axis lock comes last of
all.
Bounded lists keep their ends
A layers panel: rows move up and down only, and never leave the panel. Drag the top row to the very bottom:
Loading the demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Layers from './Layers.vue';
</script>
<template>
<DnDProvider>
<Layers />
</DnDProvider>
</template>const { items } = makeSortableList(list, { items: layers, axis: 'y', modifiers: [restrictToParent] });
A bound holds the overlay at the box's edge while the pointer goes on. Once the pointer has left the overlay along the list's axis, the list sorts by the pointer instead, so the first and the last place stay in reach however you hold the item. Grids keep sorting by the overlay: held at the edge, it already covers the edge cell.
Rules for a part of the page
Two areas, two sets of rules. Stickers in Layout snap to its grid and can't leave it; stickers in Sketch go anywhere, into Layout too. A drag keeps the rules of the area it started in, and the area whose rules are in force lights up.
Loading the demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider, type Coordinates, type Key } from '@vue-dnd-kit/core';
import DragArea from './DragArea.vue';
const stickers = ref([
{ id: 'logo', text: 'Logo', color: '#42b883', area: 'sketch', x: 20, y: 24 },
{ id: 'photo', text: 'Photo', color: '#3b82f6', area: 'sketch', x: 110, y: 96 },
{ id: 'quote', text: 'Quote', color: '#ec4899', area: 'sketch', x: 36, y: 150 },
{ id: 'title', text: 'Title', color: '#f59e0b', area: 'layout', x: 24, y: 24 },
{ id: 'button', text: 'Button', color: '#a855f7', area: 'layout', x: 120, y: 144 },
]);
const place = (area: string, id: Key, at: Coordinates) => {
const sticker = stickers.value.find((item) => item.id === id);
if (!sticker) return;
sticker.area = area;
sticker.x = Math.round(at.x);
sticker.y = Math.round(at.y);
};
</script>
<template>
<DnDProvider>
<div class="studio">
<DragArea
id="sketch"
title="Sketch"
rules="no rules"
:stickers="stickers.filter((sticker) => sticker.area === 'sketch')"
@place="place"
/>
<DragArea
id="layout"
title="Layout"
rules="snapToGrid(24) · restrictToArea"
strict
:stickers="stickers.filter((sticker) => sticker.area === 'layout')"
@place="place"
/>
</div>
</DnDProvider>
</template>
<style scoped>
.studio {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 12px;
width: 100%;
max-width: 560px;
margin: 0 auto;
}
</style><script setup lang="ts">
const area = useTemplateRef<HTMLElement>('area');
const { isDragging } = makeDragArea(area, {
modifiers: () => (props.strict ? [snapToGrid(24)] : []),
restrictToArea: () => props.strict,
});
</script>
<template>
<div ref="area" class="area" :class="{ busy: isDragging }">
<Sticker v-for="sticker in stickers" :key="sticker.id" :sticker="sticker" />
</div>
</template>
makeDragArea sets rules for the drags that start inside its element — plain draggables and
sortable items alike, which then need no rules of their own. Keep one DnDProvider for the
whole app, and give a board, a canvas or a feed an area of its own.
| Option | What it does |
|---|---|
axis | Locks the drags to one axis. A draggable's own axis, or its list's, wins. |
modifiers | Run after the provider's and before the draggable's own. |
restrictToArea | true: the drags stay inside the area's element. |
autoScroll | Auto-scroll of the area's scroll container (the element, or its closest scrollable ancestor). false turns it off there. |
activation | Activation per pointer kind, such as a longer touch press in a scrolling feed. A draggable's own wins; the provider's comes last. |
disabled | No drag starts in the area, nor in the areas inside it. Its draggables report aria-disabled. |
Every option takes a value, a ref or a getter, read when a press or a drag starts. The area
returns isDragging (a drag that started here is running, as the lit frame above shows) and
isAutoScrolling (auto-scroll is moving its container). Full reference:
makeDragArea.
How areas combine
- A drag follows the area it started in, even after it leaves it. Passing over an area changes
nothing, except its
autoScroll: that one applies to every drag that scrolls its container. - Nested areas don't merge: the closest one wins.
disabledis the exception, and it is inherited: an area that is off turns off the areas inside it too. - Collision detection, keyboard keys and announcements stay provider-wide.
DnDProvider around the board? Providers are independent: a drag never crosses
from one to another, so the board's items could never leave it. Nest a provider only when a part
of the page needs provider-wide settings of its own and its items stay inside it. See
The provider.Leave it where it's dropped
The timeline, the call, the flowchart, the slide and the areas above all keep each item where it
was dropped. Every drag event carries position: the dragged box's top-left corner inside the
target, from its padding edge and with its scroll — the left and top that put an absolutely
positioned item right there. It comes after the modifiers, so a snapped node lands on the grid.
makeDroppable(canvas, { id: 'canvas' });
useDnDMonitor({
onDragEnd({ active, over, position }) {
const node = nodes.value.find((item) => item.id === active.id);
if (!node || over?.id !== 'canvas' || !position) return;
node.x = Math.round(position.x);
node.y = Math.round(position.y);
},
});
More on free dragging and drop targets in Drag and drop.