For AI agents: the complete documentation index is available at https://gultyayev.github.io/ngx-virtual-dnd/llms.txt, the full documentation bundle is available at https://gultyayev.github.io/ngx-virtual-dnd/llms-full.txt, and this page is available as Markdown at https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md.

Drag behavior

These inputs change when a drag starts and how it moves. All of them are optional.

Only one drag runs at a time. While an item is being dragged, with the pointer or the keyboard, pressing another item (a second finger on a touch screen) or pressing Space on it does not start a second drag.

Drag handles

Set dragHandle to a CSS selector, and only presses inside a matching element in the row start a drag (an element around the row that matches doesn't count). The rest of the row stays free for selecting text, clicking and so on.

Live exampleList with drag handlesOpen in new tab
src/app/examples/drag-handle/drag-handle.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  DragPreviewComponent,
  DraggableDirective,
  DropEvent,
  DroppableGroupDirective,
  reorderItems,
  VirtualSortableListComponent,
} from 'ngx-virtual-dnd';

interface Track {
  id: string;
  title: string;
}

@Component({
  selector: 'app-drag-handle-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <div vdndGroup="playlist">
      <ng-template #trackTpl let-track>
        <!-- Only presses inside [data-drag-handle] start a drag; the rest of the row stays selectable -->
        <div class="row with-handle" [vdndDraggable]="track.id" dragHandle="[data-drag-handle]">
          <span class="handle" data-drag-handle aria-hidden="true">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
              <circle cx="9" cy="6" r="1.6" />
              <circle cx="15" cy="6" r="1.6" />
              <circle cx="9" cy="12" r="1.6" />
              <circle cx="15" cy="12" r="1.6" />
              <circle cx="9" cy="18" r="1.6" />
              <circle cx="15" cy="18" r="1.6" />
            </svg>
          </span>
          <span>{{ track.title }}</span>
        </div>
      </ng-template>

      <vdnd-sortable-list
        class="list"
        droppableId="playlist"
        [items]="tracks()"
        [itemHeight]="48"
        [containerHeight]="336"
        [itemIdFn]="trackId"
        [itemTemplate]="trackTpl"
        (drop)="onDrop($event)"
      />
    </div>

    <vdnd-drag-preview />
  `,
})
export class DragHandleExampleComponent {
  readonly tracks = signal<Track[]>(
    Array.from({ length: 300 }, (_, i) => ({ id: `track-${i + 1}`, title: `Track ${i + 1}` })),
  );

  readonly trackId = (track: Track): string => track.id;

  onDrop(event: DropEvent): void {
    reorderItems(event, this.tracks);
  }
}

Make the handle a non-interactive element such as a <span>. Presses inside a button in the row never start a drag, as the next section explains. Keyboard dragging is unaffected: the whole row stays focusable.

Interactive children

Controls inside rows keep working: pressing them never starts a drag. What starts a drag depends on where the pointer press lands, or on which element has focus when Space is pressed:

ElementPointer press starts a dragSpace while it has focus picks the row up
The row itself, even when the draggable element is a button, such as <button vdndDraggable>YesYes
The row, when the draggable element is another control, such as an input or a [contenteditable] elementNo, except from a drag handle (see below)No: it types, toggles or opens instead. A focusable drag handle inside it can
A row inside a control, such as the blocks of a [contenteditable] editorYesNot in Chromium (see below)
The row outside the handle, when dragHandle is setNoYes, unless the draggable element is a text field, select or editable element
A non-interactive drag handle, such as a <span>YesYes, when you make it focusable (tabindex="0"); otherwise it cannot take focus, and the row can
A drag handle that is a buttonNoYes
A button, input, textarea, select or [contenteditable] element inside the rowNoNo: it types or clicks instead
An element with the no-drag class, and everything inside itNoNo
The row, with no-drag on the draggable element itselfNoYes

Only controls inside the row count: a control around the list doesn't stop a drag, and neither does a draggable element that is a button (a button, or an input of type button, submit, reset or image). A draggable element that is another control, such as a text field, a checkbox, a select or a [contenteditable] element, keeps its own press and Space: a press focuses, opens, toggles or places the caret, and Space types a space, toggles or opens. An element with contenteditable="false" is not editable, so it doesn't count either.

Rows inside a [contenteditable] editor drag like any other row, so pressing one starts a drag instead of placing the caret. To keep their text editable with the mouse, give them a drag handle. The same goes for blocks that are their own editing host (<div contenteditable vdndDraggable>): give them a handle with contenteditable="false", such as a grip icon, and presses on it start a drag while the rest of the block stays editable. Space in the block's text types a space, so make the grip focusable for keyboard users, with tabindex="0", role="button" and an aria-label such as "Drag block": Space on it picks the block up. Without a focusable handle, the block can't be moved with the keyboard. After a keyboard drop or cancel, focus returns to the block itself (placing the caret in it), so Tab reaches the grip again. In Chromium, Space on a row inside an editor goes to the editor instead of picking the row up; Firefox and Safari pick it up.

Only the primary mouse button starts a drag. To turn dragging off completely, use disabled.

Other interactive elements are not recognized, for example a web component whose input is inside its shadow DOM, or an element with role="switch". Add no-drag to them. For a web component, add it to the component element itself: presses inside its shadow DOM reach the row as presses on that element.

Threshold and delay

<div [vdndDraggable]="task.id" [dragThreshold]="10" [dragDelay]="200">{{ task.title }}</div>
InputDefaultEffect
dragThreshold5How far (px) the pointer must move before the drag starts. Stops clicks from turning into drags.
dragDelay0How long (ms) the pointer must be held first. Moving past the threshold before the delay ends cancels the drag attempt, so touch users can still scroll.

When the delay has passed, the element gets the vdnd-drag-pending class. Style it so users can see the item is ready to move:

.vdnd-drag-pending {
  transform: scale(1.02);
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
}

A delay of 150–250 ms works well on touch devices.

Axis locking

<div [vdndDraggable]="task.id" lockAxis="x">{{ task.title }}</div>

lockAxis names the axis that is frozen:

  • 'x': the X axis is frozen, so the item moves only vertically
  • 'y': the Y axis is frozen, so the item moves only horizontally
Migrating from Angular CDK

This is the opposite of CDK's cdkDragLockAxis, where 'x' means the item moves along the X axis.

Constraining to the container

<vdnd-sortable-list [constrainToContainer]="true" ... />
<!-- or -->
<div vdndDroppable="tasks" [constrainToContainer]="true">...</div>

The preview cannot leave the container. Holding it against the container's top or bottom edge drops the item at the first or last position once the list is scrolled to that end; until then the drop position is the visible row at that edge, while autoscroll, when it runs, moves the list there. The container is the nearest vdndScrollable ancestor if there is one, and the droppable otherwise.

When a sticky header or footer inside the vdndScrollable element covers part of it, pass their heights as scrollInsetTop and scrollInsetBottom so the preview stays clear of them (see Sticky headers and footers).

Disabling

Every draggable, droppable and list accepts disabled:

<div [vdndDraggable]="task.id" [disabled]="task.locked">{{ task.title }}</div>

<!-- Nothing can be dropped into this list -->
<vdnd-sortable-list [disabled]="locked()" ... />
  • A disabled draggable cannot be picked up. It gets tabindex="-1" and the vdnd-draggable-disabled class.
  • A disabled droppable (or <vdnd-sortable-list>) is skipped as a target. Its items can still be dragged out, so for a read-only list also disable the draggables in its item template. The pointer passes through to whatever enabled droppable is underneath, and keyboard left/right navigation skips it. Releasing over it emits no (drop). (dragEnd) still fires with destinationIndex: null. It gets the vdnd-droppable-disabled class.