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/animations.md.

Animations and haptics

By default, rows displaced by the placeholder jump to their new position, and the preview disappears on drop. Provide VDND_ANIMATION_CONFIG to make rows slide and the dropped item glide into its slot instead. For feedback on every step, such as a haptic tick, listen to (placeholderMove).

Live exampleSliding rows and a placeholder move counterOpen in new tab
src/app/examples/shift-animation/shift-animation.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  DragPreviewComponent,
  DraggableDirective,
  DropEvent,
  DroppableGroupDirective,
  PlaceholderMoveEvent,
  reorderItems,
  VDND_ANIMATION_CONFIG,
  VirtualSortableListComponent,
} from 'ngx-virtual-dnd';

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

@Component({
  selector: 'app-shift-animation-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  // Lists in this component slide displaced rows instead of snapping them into place
  providers: [{ provide: VDND_ANIMATION_CONFIG, useValue: { shiftDuration: 200 } }],
  template: `
    <p class="status">
      Placeholder moves: <strong data-placeholder-moves>{{ moves() }}</strong>
      @if (lastMove()) {
        · last <strong data-last-placeholder-move>{{ lastMove() }}</strong>
      }
    </p>

    <div vdndGroup="tasks">
      <ng-template #taskTpl let-task>
        <div class="row" [vdndDraggable]="task.id">{{ task.title }}</div>
      </ng-template>

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

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

  readonly moves = signal(0);
  readonly lastMove = signal('');

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

  onPlaceholderMove(event: PlaceholderMoveEvent): void {
    // One event per displacement: a good place for haptic feedback
    navigator.vibrate?.(10);
    this.moves.update((count) => count + 1);
    this.lastMove.set(`${event.previousIndex ?? 'entered'} → ${event.currentIndex}`);
  }

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

Shift animation

The animation is opt-in. Provide the config app-wide, or in a component's providers to animate only the lists in that subtree:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { VDND_ANIMATION_CONFIG } from 'ngx-virtual-dnd';

export const appConfig: ApplicationConfig = {
  providers: [{ provide: VDND_ANIMATION_CONFIG, useValue: { shiftDuration: 200 } }],
};
OptionDefaultMeaning
shiftDuration200Slide duration in ms. 0 turns it off
shiftEasingcubic-bezier(0.2, 0, 0, 1)Any CSS easing function
dropDuration200Drop animation duration in ms. 0 turns it off
dropEasingcubic-bezier(0.2, 0, 0, 1)Any CSS easing function
  • The slide animates the translate CSS property, so it runs on the compositor. It works with virtual scrolling and with dynamic heights.
  • It is skipped when the user has turned on reduced motion (prefers-reduced-motion: reduce).
  • If the placeholder moves again mid-slide, rows continue from where they currently are.
  • Drop positions are calculated from geometry, so a row that is still sliding never changes where an item lands.
  • Rows also slide on drop or cancel. After a drop they usually already sit in their final slots, so a slide in progress simply finishes. After a cancel, displaced rows slide back.
  • Your own transform on rows is kept: the slide moves the row as a whole, and your rotation or scale does not distort the slide. If you set translate on rows yourself, the slide is added to it, but then it runs on the main thread instead of the compositor, so prefer transform for your own offsets. A translate you change while a row is sliding applies once the slide ends.

The config is read each time an animation starts, so a getter can switch it at runtime, for example from a user setting:

{
  provide: VDND_ANIMATION_CONFIG,
  useFactory: () => {
    const settings = inject(SettingsService);
    return {
      get shiftDuration() {
        return settings.reduceAnimations() ? 0 : 200;
      },
    };
  },
}

Drop animation

With the same config, releasing an item no longer makes the preview vanish. The preview glides from where you let go onto the item's final position, then disappears. The real item stays invisible until the preview lands on it, so it is never shown twice. A cancelled drag (Escape) glides back to where the item started.

  • It is purely visual. drop and dragEnd fire right away, and the list is already in its new order while the preview travels.
  • The preview lands on the item as it is rendered after your (drop) handler. Commit the move in the handler (for example with moveItem() or reorderItems()). If you commit later, for example after a server call, the preview glides back to the item's old slot.
  • If the item is not rendered, or is scrolled out of its list's view, the preview fades out where it was released.
  • Starting a new drag ends the animation at once.
  • While it plays, the preview box has the class vdnd-drag-preview-dropping.
  • It is skipped under reduced motion, like the shift animation. Set dropDuration: 0 to keep only the shift animation.

Where it applies

Rows animate in <vdnd-sortable-list>, <vdnd-virtual-scroll> and every *vdndVirtualFor list, including those inside <vdnd-virtual-viewport> and <vdnd-virtual-content>.

Warning

Do not add your own CSS transition on the rows' transform, translate or top to get this effect. Rows are repositioned by layout (or by an inline top in a *vdndVirtualFor placed directly in a vdndScrollable), and a transition on any of them competes with the built-in slide.

Placeholder move events

vdndDroppable and <vdnd-sortable-list> emit placeholderMove each time the placeholder moves within that list, which is every time rows are displaced:

<vdnd-sortable-list ... (placeholderMove)="onPlaceholderMove($event)" />
onPlaceholderMove(event: PlaceholderMoveEvent): void {
  navigator.vibrate?.(10); // or Haptics.selectionChanged() from @capacitor/haptics
}

The event fires when the placeholder enters the list and on every move inside it. It does not fire for the initial pick-up or when the placeholder leaves the list. currentIndex is where the item would land if it were dropped now, so it also suits screen reader announcements. See PlaceholderMoveEvent for every field.

Keep the handler cheap, and bind it only where you use it: each event runs change detection for your component and its ancestors (see Performance).