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/preview-and-placeholder.md.

Preview and placeholder

During a drag the original element is hidden. Two elements take its place:

  • the preview, which follows the pointer
  • the placeholder, an empty slot that marks where the item will land

Default preview

<vdnd-drag-preview /> clones the dragged element, copies its computed styles and sizes the clone to match the original. The point where the user grabbed the item stays under the pointer.

The clone is a picture of the item: its controls are disabled and out of the tab order. It keeps the item's id attributes, so rules that style the item through an ID style the clone too. That means the document has each of those IDs twice during a drag; see Known limitations.

The clone is made in the frame the item lifts, and copying the styles takes longer the more elements the row has. For rows with many elements on low-end devices, a custom preview is cheaper: with a template, drags skip the clone.

Custom preview

Pass a template to render your own preview instead of a clone:

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

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

@Component({
  selector: 'app-custom-preview-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <div vdndGroup="tasks">
      <ng-template #taskTpl let-task>
        <!-- vdndDraggableData is what the preview template receives -->
        <div class="row" [vdndDraggable]="task.id" [vdndDraggableData]="task">
          {{ task.title }}
        </div>
      </ng-template>

      <vdnd-sortable-list
        class="list"
        droppableId="tasks"
        [items]="tasks()"
        [itemHeight]="48"
        [containerHeight]="336"
        [itemIdFn]="taskId"
        [itemTemplate]="taskTpl"
        (drop)="onDrop($event)"
      />
    </div>

    <!-- Rendered instead of a clone of the row. Style it globally: it lives under <body> -->
    <ng-template #preview let-task>
      <div class="preview-card">
        Moving <strong>{{ task.title }}</strong>
      </div>
    </ng-template>

    <vdnd-drag-preview [previewTemplate]="preview" />
  `,
})
export class CustomPreviewExampleComponent {
  readonly tasks = signal<Task[]>(
    Array.from({ length: 1000 }, (_, i) => ({ id: `task-${i + 1}`, title: `Task ${i + 1}` })),
  );

  readonly taskId = (task: Task): string => task.id;

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

The template context:

VariableValue
$implicitThe draggable's vdndDraggableData (null if not set)
draggableIdID of the dragged item
droppableIdID of the droppable the drag started in

Set [vdndDraggableData]="task" on the draggable so the template receives the item.

The preview lives under

<body> The preview is moved into a div.vdnd-overlay-container at the end of <body>. This escapes ancestor transform, filter and perspective styles, which would otherwise break position: fixed (Ionic's ion-page is one example). Your component's styles still apply to elements in the preview template. But selectors that depend on ancestors, such as .board .card, no longer match. Style the preview by its own classes.

The preview box is inert and aria-hidden="true", with a default or a custom preview alike: nothing in it takes focus or reaches screen readers. Announce the drag yourself (see Accessibility), not with live content in the preview template.

cursorOffset (default { x: 8, y: 8 }) is only a fallback for when the grab point is unknown. Drags started by the library always know it.

Placeholder

Lists render the placeholder at the drop position, sized to the dragged item. It is an empty element with the classes vdnd-drag-placeholder and vdnd-drag-placeholder-visible. Style it globally:

.vdnd-drag-placeholder {
  box-sizing: border-box;
  border: 2px dashed #9aa4b2;
  border-radius: 8px;
}

<vdnd-placeholder> (PlaceholderComponent) is a separate indicator that accepts a custom template and that you render yourself. The built-in lists do not use it.