npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ngx-virtual-dnd

v3.0.0

Published

A performant drag-and-drop library for Angular that works seamlessly with virtual scrolling

Readme

ngx-virtual-dnd

Angular drag-and-drop library optimized for virtual scrolling. Handles thousands of items efficiently by only rendering visible elements.

Inspired by react-virtualized-dnd.

Features

  • Virtual Scrolling - Renders only visible items plus overscan buffer
  • Dynamic Item Heights - Auto-measured via ResizeObserver with O(log N) lookups
  • Smooth Drag & Drop - 60fps with RAF throttling
  • Cross-List Support - Drag between multiple lists with group filtering
  • Auto-Scroll - Configurable edge scrolling with speed/threshold control
  • Drag Handles - Restrict drag initiation to specific elements
  • Container Constraints - Constrain drag preview to container boundaries
  • Axis Locking - Lock dragging to horizontal or vertical axis
  • Custom Previews - Template-based drag preview and placeholder customization
  • Keyboard Accessible - Space to grab, arrows to move, Escape to cancel
  • Touch Support - Works with mouse and touch, with configurable delay/threshold
  • Angular 21+ - Signals, standalone components, modern patterns

Installation

npm install ngx-virtual-dnd

Quick Start

import {
  VirtualSortableListComponent,
  DroppableGroupDirective,
  DraggableDirective,
  DragPreviewComponent,
  DropEvent,
  moveItem,
} from 'ngx-virtual-dnd';

@Component({
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <!-- Item template -->
    <ng-template #itemTpl let-item>
      <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
        {{ item.name }}
      </div>
    </ng-template>

    <!-- Lists wrapped in a group -->
    <div vdndGroup="my-group">
      <vdnd-sortable-list
        droppableId="list-1"
        group="my-group"
        [items]="list1()"
        [itemHeight]="50"
        [containerHeight]="400"
        [itemIdFn]="getItemId"
        [itemTemplate]="itemTpl"
        (drop)="onDrop($event)"
      />

      <vdnd-sortable-list
        droppableId="list-2"
        group="my-group"
        [items]="list2()"
        [itemHeight]="50"
        [containerHeight]="400"
        [itemIdFn]="getItemId"
        [itemTemplate]="itemTpl"
        (drop)="onDrop($event)"
      />
    </div>

    <!-- Required: renders the dragged item preview -->
    <vdnd-drag-preview />
  `,
})
export class MyComponent {
  list1 = signal<Item[]>([...]);
  list2 = signal<Item[]>([...]);

  getItemId = (item: Item) => item.id;

  onDrop(event: DropEvent): void {
    moveItem(event, {
      'list-1': this.list1,
      'list-2': this.list2,
    });
  }
}

That's it! VirtualSortableListComponent handles placeholder positioning, sticky items during drag, and virtual scroll integration automatically.

API Overview

The library exports these main pieces (use IDE completion for full details):

Components:

  • VirtualSortableListComponent - High-level component combining droppable, virtual scroll, and placeholder
  • VirtualScrollContainerComponent - Low-level virtual scroll container
  • VirtualViewportComponent - Self-contained virtual scroll viewport
  • VirtualContentComponent - Virtual content for external scroll containers (page-level scroll)
  • DragPreviewComponent - Renders the dragged item preview (required, supports custom templates)
  • PlaceholderComponent - Drop position indicator

Directives:

  • DraggableDirective (vdndDraggable) - Makes an element draggable (supports drag handles, axis locking, threshold/delay)
  • DroppableDirective (vdndDroppable) - Marks a drop target (supports container constraints, auto-scroll config)
  • DroppableGroupDirective (vdndGroup) - Provides group context to children
  • ScrollableDirective (vdndScrollable) - Marks external scroll container
  • VirtualForDirective (*vdndVirtualFor) - Structural directive for virtual lists
  • ContentHeaderDirective (vdndContentHeader) - Marks a projected header inside VirtualContentComponent (auto-measured via ResizeObserver)

Services:

  • DragStateService - Access drag state (isDragging, draggedItem, placeholderIndex, etc.)
  • AutoScrollService - Controls edge auto-scrolling
  • PositionCalculatorService - Calculates placeholder positions

Utilities:

  • moveItem() - Move between signal-based lists
  • reorderItems() - Reorder within a single list
  • applyMove() - Immutable version (returns new arrays)
  • isNoOpDrop() - Check if drop would be a no-op
  • insertAt() / removeAt() - Low-level array helpers

Strategies:

  • VirtualScrollStrategy - Interface for custom virtual scroll strategies
  • FixedHeightStrategy - Fixed index * itemHeight math (zero overhead)
  • DynamicHeightStrategy - Variable heights with auto-measurement and binary search

Advanced Usage

Dynamic Item Heights

When items have variable heights, enable dynamicItemHeight. Items are auto-measured via ResizeObserver — no manual height tracking needed. The itemHeight value serves as the initial estimate for unmeasured items.

With VirtualSortableListComponent:

<vdnd-sortable-list
  droppableId="list-1"
  group="my-group"
  [items]="list()"
  [itemHeight]="80"
  [dynamicItemHeight]="true"
  [itemIdFn]="getItemId"
  [itemTemplate]="itemTpl"
  (drop)="onDrop($event)"
/>

With VirtualScrollContainerComponent:

<vdnd-virtual-scroll
  [items]="items()"
  [itemHeight]="80"
  [dynamicItemHeight]="true"
  [itemIdFn]="getItemId"
  [trackByFn]="trackById"
  [itemTemplate]="itemTpl"
/>

With VirtualForDirective:

<ng-container
  *vdndVirtualFor="
    let item of items();
    itemHeight: 80;
    dynamicItemHeight: true;
    trackBy: trackById;
    droppableId: 'list-1'
  "
>
  <div class="item">{{ item.description }}</div>
</ng-container>

Notes:

  • FixedHeightStrategy is used by default when dynamicItemHeight is not set
  • Setting dynamicItemHeight switches to DynamicHeightStrategy with automatic height measurement
  • Heights are tracked by trackBy key, so they survive reordering
  • The itemHeight value is used as the initial estimate for items not yet measured
  • Inside a viewport component (vdnd-virtual-viewport or vdnd-virtual-content), itemHeight, dynamicItemHeight, and droppableId are inherited automatically — only trackBy is needed on the directive

Drag Handles

Use dragHandle with a CSS selector to restrict where users can initiate a drag:

<div [vdndDraggable]="item.id" dragHandle=".handle">
  <span class="handle">⠿</span>
  <span>{{ item.name }}</span>
</div>

Only clicks on elements matching the selector will start a drag. The rest of the element remains interactive.

Container Constraints

Constrain the drag preview and placeholder to stay within the droppable container:

<vdnd-sortable-list
  droppableId="list-1"
  group="my-group"
  [items]="items()"
  [itemHeight]="50"
  [constrainToContainer]="true"
  [itemIdFn]="getItemId"
  [itemTemplate]="itemTpl"
  (drop)="onDrop($event)"
/>

Or on the directive directly:

<div vdndDroppable="list-1" [constrainToContainer]="true">...</div>

The preview cannot leave the droppable area, and the placeholder snaps to the edges.

Axis Locking

Lock dragging to a single axis:

<div [vdndDraggable]="item.id" lockAxis="y">{{ item.name }}</div>

Values: 'x' (horizontal only), 'y' (vertical only), or omit for free movement.

Drag Threshold & Delay

<div [vdndDraggable]="item.id" [dragThreshold]="10" [dragDelay]="200">{{ item.name }}</div>
  • dragThreshold — minimum distance (px) before drag starts (default: 5). Prevents accidental drags on click.
  • dragDelay — delay (ms) after pointer down before drag activates (default: 0). Useful on touch devices to distinguish scrolling from dragging. Use the vdnd-drag-pending CSS class to show visual feedback when the delay passes.

Custom Drag Preview

Provide a custom template for the drag preview:

<ng-template #preview let-data let-draggableId="draggableId">
  <div class="custom-preview">Dragging: {{ data.name }}</div>
</ng-template>

<vdnd-drag-preview [previewTemplate]="preview" [cursorOffset]="{ x: 16, y: 16 }" />
  • previewTemplate — custom template for the preview. Context provides $implicit (the draggable's data), draggableId, and droppableId.
  • cursorOffset — offset from cursor in pixels (default: { x: 8, y: 8 }).

Without a custom template, the library clones the dragged element as the preview.

Auto-Scroll Configuration

Configure auto-scroll behavior when dragging near container edges:

<vdnd-sortable-list
  droppableId="list-1"
  group="my-group"
  [items]="items()"
  [itemHeight]="50"
  [autoScrollConfig]="{ threshold: 80, maxSpeed: 20 }"
  [itemIdFn]="getItemId"
  [itemTemplate]="itemTpl"
  (drop)="onDrop($event)"
/>

| Option | Default | Description | | ------------ | ------- | ------------------------------------ | | threshold | 50 | Distance from edge (px) to trigger | | maxSpeed | 15 | Maximum scroll speed (px/frame) | | accelerate | true | Speed up based on distance from edge |

Set [autoScrollEnabled]="false" to disable auto-scroll entirely. These options are available on VirtualSortableListComponent, DroppableDirective, and ScrollableDirective.

Disabling Drag & Drop

Use the disabled input to conditionally disable draggables, droppables, or entire lists:

<!-- Disable a single item -->
<div [vdndDraggable]="item.id" [disabled]="!item.canDrag">{{ item.name }}</div>

<!-- Disable an entire list -->
<vdnd-sortable-list
  droppableId="list-1"
  group="my-group"
  [items]="items()"
  [itemHeight]="50"
  [disabled]="isReadOnly()"
  [itemIdFn]="getItemId"
  [itemTemplate]="itemTpl"
  (drop)="onDrop($event)"
/>

Disabled draggables get the vdnd-draggable-disabled CSS class. Disabled droppables get vdnd-droppable-disabled.

Low-Level API

For maximum control, use individual components instead of VirtualSortableListComponent:

@Component({
  imports: [
    VirtualScrollContainerComponent,
    DroppableGroupDirective,
    DroppableDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <ng-template #itemTpl let-item>
      <div class="item" [vdndDraggable]="item.id" [vdndDraggableData]="item">
        {{ item.name }}
      </div>
    </ng-template>

    <div vdndGroup="demo">
      <div vdndDroppable="list-1" (drop)="onDrop($event)">
        <vdnd-virtual-scroll
          droppableId="list-1"
          [items]="items()"
          [itemHeight]="50"
          [itemIdFn]="getItemId"
          [trackByFn]="trackById"
          [itemTemplate]="itemTpl"
        />
      </div>
    </div>
    <vdnd-drag-preview />
  `,
})
export class ListComponent {
  items = signal<Item[]>([]);
}

Page-Level Scroll

Use VirtualContentComponent with vdndScrollable for page-level scrolling with headers/footers:

@Component({
  imports: [
    ScrollableDirective,
    VirtualContentComponent,
    VirtualForDirective,
    DraggableDirective,
    DroppableDirective,
    DroppableGroupDirective,
    DragPreviewComponent,
    ContentHeaderDirective,
  ],
  template: `
    <ion-content [scrollY]="false">
      <div class="scroll-container ion-content-scroll-host" vdndScrollable>
        <div vdndGroup="tasks">
          <vdnd-virtual-content
            [itemHeight]="72"
            vdndDroppable="list-1"
            (drop)="onDrop($event)"
          >
            <!-- Header — auto-measured via ResizeObserver, scrolls with content -->
            <div class="header" vdndContentHeader>Welcome!</div>

            <ng-container
              *vdndVirtualFor="
                let item of items();
                trackBy: trackById
              "
            >
              <div class="item" [vdndDraggable]="item.id">{{ item.name }}</div>
            </ng-container>

          </vdnd-virtual-content>
        </div>

        <!-- Footer — normal sibling in document flow -->
        <div class="footer">Load more</div>
      </div>
    </ion-content>

    <vdnd-drag-preview />
  `,
})
export class PageComponent {
  items = signal<Item[]>([...]);
}

Key points:

  • vdndScrollable marks the scroll container
  • VirtualContentComponent provides wrapper-based positioning and derives total items from the child *vdndVirtualFor automatically
  • vdndContentHeader marks a projected header — its height is auto-measured via ResizeObserver and used as the content offset (no manual measurement needed)
  • contentOffset input is available as an escape hatch when the header lives outside the component
  • *vdndVirtualFor inherits itemHeight, dynamicItemHeight, and droppableId from the parent viewport/droppable — only trackBy is required

Screen Reader Announcements

The library emits events with position data. Implement announcements in your app:

@Component({
  template: `
    <div
      vdndDraggable="item-1"
      (dragStart)="announce('Grabbed ' + item.name)"
      (dragEnd)="announceEnd($event)"
    >
      {{ item.name }}
    </div>
    <div aria-live="assertive" class="sr-only">{{ announcement() }}</div>
  `,
})
export class MyComponent {
  announcement = signal('');

  announce(msg: string) {
    this.announcement.set(msg);
  }

  announceEnd(e: DragEndEvent) {
    this.announce(
      e.cancelled
        ? `Cancelled. Returned to position ${e.sourceIndex + 1}`
        : `Dropped at position ${e.destinationIndex! + 1}`,
    );
  }
}

Keyboard Navigation

| Key | Action | | ----------- | --------------------------- | | Tab | Navigate to draggable items | | Space | Start/end drag | | Arrow ↑/↓ | Move item up/down | | Arrow ←/→ | Move to adjacent list | | Escape | Cancel drag |

ARIA attributes (aria-grabbed, aria-dropeffect, tabindex) are managed automatically.

CSS Classes

| Class | Applied When | | ------------------------- | --------------------------- | | vdnd-draggable | Always on draggable | | vdnd-draggable-dragging | While being dragged | | vdnd-draggable-disabled | When disabled | | vdnd-drag-pending | After delay, ready to drag | | vdnd-droppable | Always on droppable | | vdnd-droppable-active | When a draggable is over it | | vdnd-droppable-disabled | When disabled |

Events

All event types are importable from ngx-virtual-dnd.

| Output | Event Type | Emitted By | | ------------- | ---------------- | ---------------------------------------------------- | | (dragStart) | DragStartEvent | DraggableDirective | | (dragEnd) | DragEndEvent | DraggableDirective | | (drop) | DropEvent | DroppableDirective, VirtualSortableListComponent |

DragEndEvent includes a cancelled boolean to distinguish drops from cancellations.

How It Works

Traditional drag-and-drop libraries query sibling DOM elements via getBoundingClientRect(). This fails with virtual scrolling because items outside the viewport aren't rendered.

This library uses element-under-point detection: temporarily hide the dragged element, use document.elementFromPoint() to find what's at the cursor, walk up to find the droppable, and calculate placeholder position mathematically.

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)

Development

npm start                    # Dev server (localhost:4200)
ng build ngx-virtual-dnd     # Build library (required after lib edits)
npm test                     # Unit tests
npm run e2e                  # E2E tests

AI Agent Skills

Install a skill to teach AI coding assistants how to integrate this library:

npx skills add gultyayev/angular-vdnd

Works with Claude Code, Cursor, Windsurf, GitHub Copilot, and 40+ other agents.

License

MIT